株式会社ゆめみ
浜名将輝
所属: 株式会社ゆめみ(2020年新卒入社)
職種: サーバサイドエンジニア
使用技術: Kotlin + SpringBoot
お仕事: LINEミニアプリのサーバサイド
マイブーム: 歌, 物件探し
欲しいもの: 電動折り畳み自転車
UIテスト
統合テスト
ユニットテスト
User Name
Password
Login
Failed!
Invalid User Name or Password.
Shoki
●●●●●●●
実際の画面遷移まで含めたテスト
Success!
Hello, Shoki.
@Controller
class UserController() {
@PostMapping("/login")
fun login(request: Request) : HTTPResponse{
val encodedPassword = encode(request.password)
val user = userService.findByNameAndPassword(request.name, encodedPassword)
if(user.isNull) {
return HTTPResponse(NOT_FOUND, ResultStrings("Failed!", "Invalid User Name or Passoword"));
} else {
return HTTPResponse(OK, ResultStrings("Success!", "Hello, ${user.name}."))
}
}
@PostMapping("/register")
fun register(request: Request) : HTTPResponse{
...
}
...
}
ビジネス的にある程度まとまった処理単位ごと = エンドポイント
※擬似コードです
@Component
class RateManager() {
fun lookuptaxRate(region: String) : Float {
if( region.isEmpty() ) {
throw MyException()
}
if( region == "Alberta" )
return 0.1
}
if( region == "Saskatchewan" )
return 0.2
}
return 0.3
}
...
}
クラスやメソッド単位ごと
※擬似コードです
UIテスト
統合テスト
ユニットテスト
広範囲
詳細
フロントエンドチームと
サーバサイドチームに明確に分かれて開発
サーバサイドはREST APIの開発をすることが多い
UIテスト
統合テスト
ユニットテスト
←やる
←やる
←やらない
自動テスティングライブラリを用いて行います
書いたコードが正しく動いているか
の確認をするためだけではありません
カオスになります
自動テストが無い所からはできれば逃げましょう
最初から100点を目指さない
TDD
自動テストを書きながら実装する
セーブしながらゲームする
ことに似ている気がしないでもない...
class UserControllerTests {
...
@BeforeEach
fun init() {
// テスト用のControllerを用意する
userControllerMock = MockMvcBuilders
.standaloneSetup(userController)
.build()
}
/* ログイン成功時のテスト */
@Test
fun loginTest_OK() {
val expectedValue = ...
// エンドポイントに対してリクエストを送る
userControllerMock.perform(MockMvcRequestBuilders.post("/login"))
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(ReqestBody(userName = "Shoki", password = "my_password")
// ステータスコードが200 OKかの確認
).andExpect(status().isOk)
// Content-Typeが想定通りの型かどうかの確認
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
// レスポンスの内容が期待通りの値かどうかの確認
.andExpect(content().json(convertToJsonString(expectedValue)))
}
}
class UserControllerTests {
...
@BeforeEach
fun init() {
// テスト用のControllerを用意する
userControllerMock = MockMvcBuilders
.standaloneSetup(userController)
.build()
}
/* ログイン失敗時のテスト */
@Test
fun loginTest_NOT_FOUND() {
val expectedValue = ...
// エンドポイントに対してリクエストを送る
userControllerMock.perform(MockMvcRequestBuilders.post("/login"))
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(ReqestBody(userName = "Shoki", password = "invalid_password")
// ステータスコードが404 NOT FOUNDかの確認
).andExpect(status().isNotFound)
// Content-Typeが想定通りの型かどうかの確認
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
// レスポンスの内容が期待通りの値かどうかの確認
.andExpect(content().json(convertToJsonString(expectedValue)))
}
}
class RateMaganerTests {
@Autowired
lateinit var rateManager: RateManager
/*デフォルト値を取るテスト*/
@Test
fun defaultBehaviorTest() {
asserEquals (0.3, rateManager.lookupTaxRate("Somewhere"))
}
/*特殊ケースのテスト*/
@Test
fun specialCasesTest() {
asserEquals (0.1, rateManager.lookupTaxRate("Alberta"))
asserEquals (0.2, rateManager.lookupTaxRate("Saskatchewan"))
}
/*例外テスト*/
@Test(expected = MyException::class)
fun testIllegalValueTest() {
rateManager.lookupTaxRate("")
}
}
class RateMaganerTests {
...
companion object {
val EXPECTATION_DEFAULT = 0.3
val EXPECTATION_ALBERTA = 0.1
val EXPECTATION_SASKATCHEWAN = 0.2
val INPUT_DEFAULT_CASE = "Somewhere"
val INPUT_SPECIAL_CASE_ALBERTA = "Alberta"
val INPUT_SPECIAL_CASE_SASKACHEWAN = "Saskatchewan"
val INPUT_EMPTY_REGION = ""
}
...
/*デフォルト値を取るテスト*/
@Test
fun defaultBehaviorTest() {
asserEquals (EXPECTATION_DEFAULT, rateManager.lookupTaxRate(INPUT_DEFAULT_CASE))
}
/*特殊ケースのテスト*/
@Test
fun specialCasesTest() {
asserEquals (EXPECTATON_ALBERTA, rateManager.lookupTaxRate(INPUT_SPECIAL_CASE_ALBERTA))
asserEquals (EXPECTATON_SASKATCHEWAN, rateManager.lookupTaxRate(INPUT_SPECIAL_CASE_SASKACHEWAN))
}
/*例外テスト*/
@Test(expected = MyException::class)
fun testIllegalValueTest() {
rateManager.lookupTaxRate(INPUT_EMPTY_REGION)
}
}
テストコードが増えると
可読性が下がる...
class RateMaganerTests {
@Autowired
lateinit var rateManager: RateManager
...
@Test
fun `デフォルト値を取る時のテスト`() {
asserEquals (0.3, rateManager.lookupTaxRate("Somewhere"))
}
@Test
fun `特殊ケースのテスト`() {
asserEquals (0.1, rateManager.lookupTaxRate("Alberta"))
asserEquals (0.2, rateManager.lookupTaxRate("Saskatchewan"))
}
@Test(expected = MyException::class)
fun `例外で落とすケースのテスト`() {
rateManager.lookupTaxRate("")
}
}
英語で書いて見通しが悪くなるぐらいなら
日本語も選択肢にする
class UserControllerTests {
...
/* ログイン失敗時のテスト */
@Test
fun loginTest_NOT_FOUND() {
val expectedValue = ...
// エンドポイントに対してリクエストを送る
userControllerMock.perform(MockMvcRequestBuilders.post("/login"))
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(ReqestBody(userName = "Shoki", password = "invalid_password")
// ステータスコードが404 NOT FOUNDかの確認
).andExpect(status().isNotFound)
// Content-Typeが想定通りの型かどうかの確認
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
// レスポンスの内容が期待通りの値かどうかの確認
.andExpect(content().json(convertToJsonString(expectedValue)))
}
}
userName | password |
---|---|
"Shoki" | "invalid_password" |
"invalid_userName" | "valid_password" |
"invalid_userName" | "invalid_password" |
←同じ結果で複数の入力
を試したい時
全部404の入力
class UserControllerTests {
...
/* loginで404を返すパラメータ群 */
@JvmStatic
fun notFoundLoginParams(): Stream<Arguments> = Stream.of(
arguments("Shoki", "invalid_password"),
arguments("invalid_user", "valid_password"),
arguments("invalid_user", "invalid_password")
)
}
/* ログイン失敗時のテスト 上記3通りを順番に試してくれる*/
@ParameterizedTest
@MethodSource("notFoundLoginParams")
fun loginTest_NOT_FOUND(inputUserName: String, inputPassword: String) {
val expectedValue = ...
// エンドポイントに対してリクエストを送る
userControllerMock.perform(MockMvcRequestBuilders.post("/login"))
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(ReqestBody(userName = inputUserName, password = inputPassword)
// ステータスコードが404 NOT FOUNDかの確認
).andExpect(status().isNotFound)
// Content-Typeが想定通りの型かどうかの確認
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
// レスポンスの内容が期待通りの値かどうかの確認
.andExpect(content().json(convertToJsonString(expectedValue)))
}
}
外部サービスを叩くAPIのテスト
↓
↓
Mock ...
テストを1つ書く
全てのテストを実行し、新規追加分は失敗することを確認する
実装を追加する
全てのテストが通ることを確認する
リファクタリングを行う
通常の電卓: "1+2 =" で'3'が表示
逆ポーランド記法電卓:
1 ENTER 2 ENTER + ENTERで'3'が表示
class CalcTest {
@Test
fun testInputSingleValue() {
val calc = Calc()
calc.inputValue(1)
assertEquals(1, calc.result())
}
}
テストは失敗!
そもそもコンパイルも通らない
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.firstOrNull
}
}
実装を追加してテストを通そう
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
@Test
fun testInputSingleValue() {
val calc = Calc()
calc.inputValues(1)
assertEquals(1, calc.result())
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.firstOrNull
}
}
リファクタできる箇所は特になさそう
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
@Test
fun testInputSingleValue() {
val calc = Calc()
calc.inputValues(1)
assertEquals(1, calc.result())
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.firstOrNull
}
}
新規追加分のテストは失敗!
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
@Test
fun testInputSingleValue() {
val calc = Calc()
calc.inputValues(1)
assertEquals(1, calc.result())
}
@Test
fun testInputTwoValues() {
val calc = Calc
calc.inputValues(1)
calc.inputValues(2)
assertEquals(2, calc.result())
// actual -> 1
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.lastOrNull
}
}
実装を変えてテストが通ることを確認
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
@Test
fun testInputSingleValue() {
val calc = Calc()
calc.inputValues(1)
assertEquals(1, calc.result())
}
@Test
fun testInputTwoValues() {
val calc = Calc()
calc.inputValues(1)
assertEquals(1, calc.result())
calc.inputValues(2)
assertEquals(2, calc.result())
// actual -> 1
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.lastOrNull
}
}
共通化できそうなところはする
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
lateinit var calc: Calc
@BeforeEach
fun beforeEachTest() {
calc = Calc()
}
@Test
fun testInputSingleValue() {
calc.inputValues(1)
assertEquals(1, calc.result())
}
@Test
fun testInputTwoValues() {
calc.inputValues(1)
assertEquals(1, calc.result())
calc.inputValues(2)
assertEquals(2, calc.result())
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.lastOrNull
}
}
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
lateinit var calc: Calc
@BeforeEach
fun beforeEachTest() {
calc = Calc()
}
@Test
fun testInputSingleValue() {
<中略>
}
@Test
fun testInputTwoValues() {
<中略>
}
@Test
fun testAddTwoValues() {
calc.inputValues(1)
calc.inputValues(2)
calc.add()
assertEquals(3, calc.result())
}
}
新規追加分のテストは失敗!
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
lateinit var calc: Calc
@BeforeEach
fun beforeEachTest() {
calc = Calc()
}
@Test
fun testInputSingleValue() {
<中略>
}
@Test
fun testInputTwoValues() {
<中略>
}
@Test
fun testAddTwoValues() {
calc.inputValues(1)
calc.inputValues(2)
calc.add()
assertEquals(3, calc.result())
}
}
テストが通ることを確認!
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.lastOrNull
}
fun add() {
val first = stack.removeFirst()// pop
val secont = stack.removeFirst()
val result = first + second
stack.add(result)
}
}
class Calc {
val stack = mutableListOf<Int>()
fun inputValue(value: Int) {
stack.add(value)
}
fun result() : Int? {
return stack.lastOrNull
}
fun add() {
val first = stack.removeFirst()// pop
val secont = stack.removeFirst()
val result = first + second
stack.add(result)
}
}
1. テストを1つ書く
2. 全てのテストを実行&新規追加分は失敗
3. 実装を追加する
4. 全てのテストが通ることを確認する
5. リファクタリングを行う
class CalcTest {
lateinit var calc: Calc
@BeforeEach
fun beforeEachTest() {
calc = Calc()
}
@Test
fun testInputSingleValue() {
<中略>
}
@Test
fun testInputTwoValues() {
<中略>
}
@Test
fun testAddTwoValues() {
calc.inputValues(1)
calc.inputValues(2)
calc.add()
assertEquals(3, calc.result())
}
}
リファクタリングするところは無さそう
個人的には最初からTDDやらずに
途中からやります
安心するために自動テストを書きましょう