まだ JUnit を使ってるの?
Kotest を使って
快適にテストを書こう
KotlinFest 2024 @hktechno
Hirotaka Kawata - @hktechno
大規模な Web サービスの裏側を Server-Side Kotlin で開発
● 2024年4月より無職
○ 7月からまた働きます (Kotlin 使うよ)
● Server-side Kotlin の経験
○ メッセンジャーアプリのバックエンド開発
■ チャットボット API のリアーキテクチャ
■ 国内最大規模のメッセージ配信の裏側を Java -> Kotlin に
■ ユニットテスト、API の End-to-end test を Kotest で作成
○ フードデリバリーサービス開発
■ 新規リアーキテクチャ案件に Kotlin・Kotest を採用して開発
Kotlin / Java なバックエンドエンジニア
Kotlin におけるテスト事情
何を使ってテスト書いていますか? Assertion に使うライブラリは?
JUnit ? hamcrest? AssertJ?
ストレス抱えてませんか?
もっと Kotlin native な強力なアサーションができたらなぁ。
そんなあなたに、
テストも Kotlin 風に書きたい!
Kotlin に慣れきった体に Java はつらいよ
Kotlin で JUnit (hamcrest) つらくないですか?
明日からはこんな感じにテスト書いてみたくないですか?
@Test
fun resultBodyClazzTest {
assertThat(result!!.body, `is`(instanceOf(Image::class.java)))
}
context(“result body”) {
should(“return image”) {
result.shouldNotBeNull()
.body.shouldBeInstanceOf<Image>()
JUnit5 (hamcrest)
Kotest
Kotest とは?
● Kotlin native なテストライブラリ・フレームワーク
○ ScalaTest の影響を強く受けている
○ Kotlin Multiplatform 対応
● 複数の機能 (後述) が独立、必要な機能だけを導入できる
○ JUnit の代わりになるテストフレームワーク全体も提供
○ 必ずしも Kotest の Spec を使う必要はない
● Kotlin と親和性の高い強力なアサーション
○ テストメソッドが拡張関数で提供される
○ Kotest DSL による記法も可能
● Coroutines や非同期なコードのテストのための機能も充実
Kotest vs JUnit 5
● JUnit5
○ Java のユニットテストが大前提
○ Kotlin 対応はとりあえずある (Java 風味)
○ 標準の Assertions や hamcrest は、機能不足
● Kotest
○ Kotlin native で書きやすく・読みやすく
■ Kotlin の型が前提の、強力な Assertion Library
■ 拡張関数を多用して、気軽に書ける Assertion
■ Lambda を使った柔軟な Assertion
○ Test framework まで Kotest を使うとさらに Kotlin っぽく
競合との比較
● AssertJ
○ Java では一般的な Fluent assertion ライブラリ
○ やっぱり、Kotlin 対応が弱い
● hamkrest
○ あくまで、hamcrest 風味のアサーション
○ 正直あまり変わり映えしない、Java 風味
● Strikt
○ expectThat(subject) から始まる Fluent assertion
○ Kotest ほど機能豊富ではなさそう
○ Fluent assertion が好きな場合にはありかも?
明日から使える Kotest
Kotest の機能は大きく分けて3つ
● Test Framework
○ JUnit のような、Kotlin native なテストフレームワーク全体
● Assertions Library
○ hamcrest や AssertJ の代わりになる Kotlin native なアサーション
● Property Testing
○ プロパティベーステストのための仕組み
全部を使わなくてもいいんです
特に、Assertions Library は明日からでも使えます!
Assertions Library
Kotest の Assertions Library
should___() というメソッドが基本
複数の条件を一度に指定も可能
result.shouldBe(expected) // 拡張関数
// or
result shouldBe expected // Kotest DSL
str.shouldContain("Kotlin")
.shouldHaveMinLength(6)
.shouldHaveMaxLength(10)
さよなら assertEquals()
Kotlin という文字列を含んだ、
6文字以上10文字以下の文字列
とりあえず、IDE で .should してみよう
めちゃくちゃ楽
Kotest - Assertions Library の導入
Gradle の場合、以下を build.gradle.kts に追加
JUnit や hamcrest と共存可能、今あるテストを書き換える必要なし
testImplementation('io.kotest:kotest-assertions-core:$version')
明日から使いたくなる強力な Assertions
● 一部を除いたのフィールドが同一であることをチェックしたい
○ あるフィールドは更新されるがテストには関係ない
○ ひとつづつフィールドをチェックするのはとても面倒
val userA = User(
name = “Kotlin”, ..., updatedAt = null, createdAt = null
)
// updatedAt と createdAt が DB 上で更新された値が返される
val saved = userRepository.save(userA)
saved.shouldBe(userA) // Fail: updatedAt と createdAt が違う
明日から使いたくなる強力な Assertions
● .shouldBeEqualToIgnoringFields() を使うと
○ 特定のフィールドのみを無視して比較してくれる
val userA = User(
name = “Kotlin”, ..., updatedAt = null, createdAt = null
)
// updatedAt と createdAt が DB 上で更新された値が返される
val savedUserA = userRepository.save(userA)
// Kotlin の Relection で Property reference を指定可能
savedUserA.shouldBeEqualToIgnoringFields(
userA, User::updateAt, User::createdAt
)
Assertions Library - Inspectors
こんなテスト書いてませんか?もし複数のテストが並列に流れたら?
val message = user.getMessages().first()
message.type.shouldBe(MessageType.TEXT)
message.text.shouldBeStartWith(“Kotlin”)
userA.sendMessage(to = userB, ...)
userB.getMessages().shouldBeEmpty()
本当に first() でいい?
メッセージ2つあるかも?
メッセージが届いてないこと
を確認したいが?
Assertions Library - Inspectors
Inspectors を使うと collection のテストも楽に
user.getMessages().forOne {
it.type.shouldBe(MessageType.TEXT)
it.text.shouldStartWith("Kotlin")
}
user.getMessages().forNone {
it.type.shouldBe(MessageType.UNKNOWN)
}
forOne - Collection の中に
ひとつだけマッチする
場合のみ成功
forNone - Collection の中に
マッチする物がない
場合のみ成功
Assertion 結果の分かりやすさ
2 elements passed but expected 1
The following elements passed:
[0] Message(type=TEXT, text=Kotlin)
[3] Message(type=TEXT, text=KotlinFest)
The following elements failed:
[1] Message(type=TEXT, text=Java) => "Java" should start
with "Kotlin" (diverged at index 0)
[2] Message(type=TEXT, text=Kotest) => "Kotest" should start
with "Kotlin" (diverged at index 3)
forOne - Collection の中に
ひとつだけマッチする
場合のみ成功
非同期なテストの例
例えばこんな例、どうやってテストしますか?
// 非同期処理: 送信や処理に時間がかかる
val messageId = userA.sendMessage(userB, message)
// 直後に取得すると失敗する
userB.getMessage(messageId).shouldNotBeEmpty() // => Fail
// 非同期処理: 送信や処理に時間がかかる
// D は C をブロックしているのでメッセージが届かないことを確認したい
val messageId = userC.sendMessage(userD, message)
userD.getMessage(messageId).shouldBeEmpty() // 本当にそれでいい?
処理に時間がかかる例
間違った場合も成功になりがちな例
非同期テスト - Eventually
● eventually を使うと、一定時間の間に成功するか判断できる
○ coroutines を使った非同期処理を書くと発生しがちなケース
○ 繰り返し間隔などの設定も可能
// 非同期処理: 送信や処理に時間がかかる
val messageId = userA.sendMessage(userB, message)
// 10秒間の間に正しい結果に変わったら、成功とみなす
eventually(10.seconds) {
userB.getMessage(messageId).shouldNotBeEmpty()
}
非同期テスト - Continually
● continually を使うと、一定時間以上条件が継続することをテスト
○ 内部では、一定時間でループして条件をチェックし続ける
○ 実装には coroutines を使っている
● その他、Retry や Until といったヘルパーもある
// 非同期処理: 送信や処理に時間がかかる
// D は C をブロックしているのでメッセージが届かないことを確認したい
val messageId = userC.sendMessage(userD, message)
continually(1.seconds) { // 1秒の間メッセージが受信されない事
userD.getMessage(messageId).shouldBeEmpty()
}
Clue (手がかり、ヒント)
● 何故失敗したか理解困難なテスト結果に遭遇したことは?
○ テスト対象フィールド以外の情報がない
■ オブジェクトのほかのフィールドが見たい
■ 結果の元になったリクエスト情報が見たい
○ 壊れやすく安定しないテスト (flaky test) で情報が足りない
■ 再現も難しいのに、良く壊れてイライラする
val response = user.sendMessage(message)
response.isSuccess.shouldBeTrue() // => Fail なぜ?
message, response が見たい!
Clue (手がかり、ヒント)
● Kotest の Clue を使うと、テスト結果に情報が付加できる
○ withClue: 任意の String を Clue として設定
○ asClue: 任意のオブジェクトを Clue として設定
withClue(“Send: ${message}”) {
user.sendMessage(message).asClue {
it.isSuccess.shouldBeTrue()
}
}
Send: Message(type=MessageType.TEXT, text=”Kotlin”)
SendMessageResponse(isSuccess=false, body=”Unauthorized”)
expected:<true> but was:<false>
Output
Matcher Modules
様々な形式や Kotlin ライブラリ向けの Matcher も用意
● JSON - io.kotest:kotest-assertions-json
● Ktor - io.kotest.extensions:kotest-assertions-ktor
● kotlinx.time - io.kotest.kotest-assertions-kotlinx-time
jsonResponse.shouldEqualJson("""{ "a": true }""")
response.shouldHaveHeader(“Server”, “Ktor”)
instant.shouldBeBefore(Clock.System.now())
Property based testing
Property based testing
● ユニットテストちゃんと書けてますか?それテストになってます?
○ エッジケース網羅できてます?
● 決まった入力パターンだけでテストしてませんか?
○ テストパターンを作成するのが面倒くさい
val bookA = Book(
name = “test book”, category = COMPUTER, pages = 100
)
shouldNotThrowAny {
bookService.createBook(bookA)
}
決め打ちのパラメータ
これ意味ある?
Property based testing
● checkAll(): 与えられた Generator を基にテストを評価
● Arb: 任意の無作為な (arbitary) 値を生成する Generator
checkAll(
iterations = 10,
Arb.string(), // ランダム文字列
Arb.<Category>enum().filter { it != UNKNOWN }, // Enum も
Arb.int(1..1000) // ランダム整数(エッジケース自動生成)
) { name, category, pages ->
val bookA = Book(name, category, pages)
shouldNotThrowAny {
bookService.createBook(bookA)
}
}
使いこなすと便利
Property based testing
● Arb はエッジケースを自動生成する (事が多い)
○ 例: Arb.string(minSize = 0, maxSize = 100)
■ 文字列の長さ (min, max) などが指定された場合、
自動的にそのエッジケースの長さが含まれる
■ エッジケースのテストを別に作る必要がない!
○ 手動でエッジケースを与えることもできる
● Arb は Fail したときに自動で Shrinking する (場合がある)
○ 文字列長が max では通らない場合、少しづつ減らして確認
○ 邪魔になることもあるので、無効化することも可能
● Data driven testing もあるが、Property testing がおすすめ
Arb の例 - Property based testing
● 標準の Arb
○ Arb.string() 文字列
○ Arb.int(1..100) 1~100 の整数
○ Arb.uuid() UUID
○ Arb.enum<EnumType>() Enum の値
○ Arb.domain() ドメイン名
● Extra arbs - io.kotest.extensions:kotest-property-arbs
○ Arb.firstName() 名前
○ Arb.wines() ワイン data class(産地・種類・農場)
○ Arb.harryPotterCharacter() ハリーポッターの登場人物
Test Framework
Coroutines in JUnit 5
● `runTest` 使ってますか?
○ え、`runBlocking` 使ってるんですか?
○ 使い分けが重要: runTest は delay を無視してくれたり
● なんか面倒ですよね?
@Test
suspend fun testA() { ... } // Error
@Test
fun testA() = runTest { ... } // OK
テストケースの名前に満足してますか?
● テストケースのメソッド名は長くなりがち
○ メソッド名が適当で何やってるかわからない等
class UserServiceTest {
@Test
suspend fun userCantSendTextMessageToBlockedUserTest() { .. }
}
文字列で書けたらなぁ
Kotest の Test Framework
● 以下の例は StringSpec
○ 他にもいくつかのスタイル (Spec) が用意されている
○ JUnit 風味の Spec もある (AnnotationSpec)
● テキストでテスト名を書くので、テスト結果が分かりやすい!
class UserServiceTest : StringSpec({
“User can’t send text message to blocked user” {
user.sendMessage(...) // coroutines
...
}
}) テストメソッドは
標準で suspend fun
StringSpec:
テスト名を文字列で管理
テストケース、構造化してますか?
class UserServiceTest {
@Nested
class UserServiceMessageTest {
@Test
suspend fun sendMessageTest() { .. }
}
@Nested
class UserServiceFriendTest {
@Test
suspend fun followTest() { .. }
}
}
JUnit でも @Nested で
きるけど...
Test Framework - 構造化テスト
● ShouldSpec
○ should から始まるテスト名を強制できる
○ 他にも構造化テストケースに対応した Spec が多数
class UserServiceTest : ShouldSpec({
context(“send message”) {
should(“not send message to blocked user”) {
user.sendMessage(...)
...
}
}
})
何を目的としたテストであるかわかりやすい!
Test Spec - Test Framework
● FunSpec
● DescribeSpec
● ShouldSpec
● StringSpec
● BehaviorSpec
● FreeSpec
● WordSpec
● FeatureSpec
● ExpectSpec
● AnnotationSpec
沢山ありすぎて、
全部紹介できないよ!
● おすすめ
○ ShouldSpec
○ WordSpec
○ ExpectSpec
○ テストの命名時に、
“何を期待しているか”
を強制できる
Test Framework の導入
● JVM であれば、JUnit runner を使うのが標準
○ つまり JUnit の上で kotest の Test Framework が動く
○ 既存の JUnit テストと共存が可能
● Multiplatform の場合は、kotest-framework-engine を使う
● IntelliJ Plugin もあるので入れておきましょう
testImplementation('io.kotest:kotest-runner-junit5:$version')
test {
useJUnitPlatform()
}
Kotest の Test Framework
● 柔軟なテストケースの生成
○ テストの名前の自由度
○ 構造化したテストケース
○ 動的なテストケースの生成
○ テストケースごとの詳細な設定 (例: timeout, enabledIf)
● Coroutines 対応
○ test method が標準で suspend fun
● 並列テストの設定
○ Coroutines で実行するため JUnit より柔軟 & 高速に
● など、様々なメリット...
その他 - Test Framework
● Mocking
○ mockk や mockito-kotlin を使ってください
○ ただし、標準では Spec 全体が Singleton のため注意
■ mock のリセットが必要
■ afterTest { clearMocks(repository) }
● @SpringBootTest - io.kotest.extensions:kotest-extensions-spring
○ Bean の constructor injection ができる (さよなら lateinit var)
@SpringBootTest
class ControllerUnitTest(
@MockkBean private val service: SomeService,
) : StringSpec({
Kotest を使ってみた感想
何だかんだ5年以上 Kotest を業務で使って見た感想 (kotlintest 時代から)
● 最近はとても安定している (以前は...)
● サービスの網羅的な E2E テスト・外形監視にも利用した
○ ユニットテストは違う観点が必要、豊富な機能が役に立つ
● チームメンバーからの評価も上々
一方で...
● Kotlin のバージョンアップで壊れることがある
○ kotest はすぐ Kotlin のバージョンに追従する && 新機能を使う
○ Kotlin のバージョンアップが必須な事が多い
● Test Framework を移行するかは、必要な機能との兼ね合いで判断
まとめ
● Kotest は素晴らしいテストフレームワークである
○ JUnit からも段階的に置き換えられるので、明日から使える
● 強力なアサーションは、テストの信頼性を上げる
○ 人間は難しいことに直面すると手抜きしがち
○ Kotest の豊富な機能で、脆弱なテストをなくす
● Property-based testing を活用すると、さらに堅牢なテストに
○ 固定のテストパターンは無意味
○ 楽してエッジケースを網羅できる
● Kotest の Test Framework を使って、もっと Kotlin らしく
○ 謎のメソッド名のテストケースを、わかりやすく

Kotest を使って 快適にテストを書こう - KotlinFest 2024

  • 1.
    まだ JUnit を使ってるの? Kotestを使って 快適にテストを書こう KotlinFest 2024 @hktechno
  • 2.
    Hirotaka Kawata -@hktechno 大規模な Web サービスの裏側を Server-Side Kotlin で開発 ● 2024年4月より無職 ○ 7月からまた働きます (Kotlin 使うよ) ● Server-side Kotlin の経験 ○ メッセンジャーアプリのバックエンド開発 ■ チャットボット API のリアーキテクチャ ■ 国内最大規模のメッセージ配信の裏側を Java -> Kotlin に ■ ユニットテスト、API の End-to-end test を Kotest で作成 ○ フードデリバリーサービス開発 ■ 新規リアーキテクチャ案件に Kotlin・Kotest を採用して開発 Kotlin / Java なバックエンドエンジニア
  • 3.
    Kotlin におけるテスト事情 何を使ってテスト書いていますか? Assertionに使うライブラリは? JUnit ? hamcrest? AssertJ? ストレス抱えてませんか? もっと Kotlin native な強力なアサーションができたらなぁ。 そんなあなたに、 テストも Kotlin 風に書きたい!
  • 4.
    Kotlin に慣れきった体に Javaはつらいよ Kotlin で JUnit (hamcrest) つらくないですか? 明日からはこんな感じにテスト書いてみたくないですか? @Test fun resultBodyClazzTest { assertThat(result!!.body, `is`(instanceOf(Image::class.java))) } context(“result body”) { should(“return image”) { result.shouldNotBeNull() .body.shouldBeInstanceOf<Image>() JUnit5 (hamcrest) Kotest
  • 5.
    Kotest とは? ● Kotlinnative なテストライブラリ・フレームワーク ○ ScalaTest の影響を強く受けている ○ Kotlin Multiplatform 対応 ● 複数の機能 (後述) が独立、必要な機能だけを導入できる ○ JUnit の代わりになるテストフレームワーク全体も提供 ○ 必ずしも Kotest の Spec を使う必要はない ● Kotlin と親和性の高い強力なアサーション ○ テストメソッドが拡張関数で提供される ○ Kotest DSL による記法も可能 ● Coroutines や非同期なコードのテストのための機能も充実
  • 6.
    Kotest vs JUnit5 ● JUnit5 ○ Java のユニットテストが大前提 ○ Kotlin 対応はとりあえずある (Java 風味) ○ 標準の Assertions や hamcrest は、機能不足 ● Kotest ○ Kotlin native で書きやすく・読みやすく ■ Kotlin の型が前提の、強力な Assertion Library ■ 拡張関数を多用して、気軽に書ける Assertion ■ Lambda を使った柔軟な Assertion ○ Test framework まで Kotest を使うとさらに Kotlin っぽく
  • 7.
    競合との比較 ● AssertJ ○ Javaでは一般的な Fluent assertion ライブラリ ○ やっぱり、Kotlin 対応が弱い ● hamkrest ○ あくまで、hamcrest 風味のアサーション ○ 正直あまり変わり映えしない、Java 風味 ● Strikt ○ expectThat(subject) から始まる Fluent assertion ○ Kotest ほど機能豊富ではなさそう ○ Fluent assertion が好きな場合にはありかも?
  • 8.
    明日から使える Kotest Kotest の機能は大きく分けて3つ ●Test Framework ○ JUnit のような、Kotlin native なテストフレームワーク全体 ● Assertions Library ○ hamcrest や AssertJ の代わりになる Kotlin native なアサーション ● Property Testing ○ プロパティベーステストのための仕組み 全部を使わなくてもいいんです 特に、Assertions Library は明日からでも使えます!
  • 9.
  • 10.
    Kotest の AssertionsLibrary should___() というメソッドが基本 複数の条件を一度に指定も可能 result.shouldBe(expected) // 拡張関数 // or result shouldBe expected // Kotest DSL str.shouldContain("Kotlin") .shouldHaveMinLength(6) .shouldHaveMaxLength(10) さよなら assertEquals() Kotlin という文字列を含んだ、 6文字以上10文字以下の文字列
  • 11.
    とりあえず、IDE で .shouldしてみよう めちゃくちゃ楽
  • 12.
    Kotest - AssertionsLibrary の導入 Gradle の場合、以下を build.gradle.kts に追加 JUnit や hamcrest と共存可能、今あるテストを書き換える必要なし testImplementation('io.kotest:kotest-assertions-core:$version')
  • 13.
    明日から使いたくなる強力な Assertions ● 一部を除いたのフィールドが同一であることをチェックしたい ○あるフィールドは更新されるがテストには関係ない ○ ひとつづつフィールドをチェックするのはとても面倒 val userA = User( name = “Kotlin”, ..., updatedAt = null, createdAt = null ) // updatedAt と createdAt が DB 上で更新された値が返される val saved = userRepository.save(userA) saved.shouldBe(userA) // Fail: updatedAt と createdAt が違う
  • 14.
    明日から使いたくなる強力な Assertions ● .shouldBeEqualToIgnoringFields()を使うと ○ 特定のフィールドのみを無視して比較してくれる val userA = User( name = “Kotlin”, ..., updatedAt = null, createdAt = null ) // updatedAt と createdAt が DB 上で更新された値が返される val savedUserA = userRepository.save(userA) // Kotlin の Relection で Property reference を指定可能 savedUserA.shouldBeEqualToIgnoringFields( userA, User::updateAt, User::createdAt )
  • 15.
    Assertions Library -Inspectors こんなテスト書いてませんか?もし複数のテストが並列に流れたら? val message = user.getMessages().first() message.type.shouldBe(MessageType.TEXT) message.text.shouldBeStartWith(“Kotlin”) userA.sendMessage(to = userB, ...) userB.getMessages().shouldBeEmpty() 本当に first() でいい? メッセージ2つあるかも? メッセージが届いてないこと を確認したいが?
  • 16.
    Assertions Library -Inspectors Inspectors を使うと collection のテストも楽に user.getMessages().forOne { it.type.shouldBe(MessageType.TEXT) it.text.shouldStartWith("Kotlin") } user.getMessages().forNone { it.type.shouldBe(MessageType.UNKNOWN) } forOne - Collection の中に ひとつだけマッチする 場合のみ成功 forNone - Collection の中に マッチする物がない 場合のみ成功
  • 17.
    Assertion 結果の分かりやすさ 2 elementspassed but expected 1 The following elements passed: [0] Message(type=TEXT, text=Kotlin) [3] Message(type=TEXT, text=KotlinFest) The following elements failed: [1] Message(type=TEXT, text=Java) => "Java" should start with "Kotlin" (diverged at index 0) [2] Message(type=TEXT, text=Kotest) => "Kotest" should start with "Kotlin" (diverged at index 3) forOne - Collection の中に ひとつだけマッチする 場合のみ成功
  • 18.
    非同期なテストの例 例えばこんな例、どうやってテストしますか? // 非同期処理: 送信や処理に時間がかかる valmessageId = userA.sendMessage(userB, message) // 直後に取得すると失敗する userB.getMessage(messageId).shouldNotBeEmpty() // => Fail // 非同期処理: 送信や処理に時間がかかる // D は C をブロックしているのでメッセージが届かないことを確認したい val messageId = userC.sendMessage(userD, message) userD.getMessage(messageId).shouldBeEmpty() // 本当にそれでいい? 処理に時間がかかる例 間違った場合も成功になりがちな例
  • 19.
    非同期テスト - Eventually ●eventually を使うと、一定時間の間に成功するか判断できる ○ coroutines を使った非同期処理を書くと発生しがちなケース ○ 繰り返し間隔などの設定も可能 // 非同期処理: 送信や処理に時間がかかる val messageId = userA.sendMessage(userB, message) // 10秒間の間に正しい結果に変わったら、成功とみなす eventually(10.seconds) { userB.getMessage(messageId).shouldNotBeEmpty() }
  • 20.
    非同期テスト - Continually ●continually を使うと、一定時間以上条件が継続することをテスト ○ 内部では、一定時間でループして条件をチェックし続ける ○ 実装には coroutines を使っている ● その他、Retry や Until といったヘルパーもある // 非同期処理: 送信や処理に時間がかかる // D は C をブロックしているのでメッセージが届かないことを確認したい val messageId = userC.sendMessage(userD, message) continually(1.seconds) { // 1秒の間メッセージが受信されない事 userD.getMessage(messageId).shouldBeEmpty() }
  • 21.
    Clue (手がかり、ヒント) ● 何故失敗したか理解困難なテスト結果に遭遇したことは? ○テスト対象フィールド以外の情報がない ■ オブジェクトのほかのフィールドが見たい ■ 結果の元になったリクエスト情報が見たい ○ 壊れやすく安定しないテスト (flaky test) で情報が足りない ■ 再現も難しいのに、良く壊れてイライラする val response = user.sendMessage(message) response.isSuccess.shouldBeTrue() // => Fail なぜ? message, response が見たい!
  • 22.
    Clue (手がかり、ヒント) ● Kotestの Clue を使うと、テスト結果に情報が付加できる ○ withClue: 任意の String を Clue として設定 ○ asClue: 任意のオブジェクトを Clue として設定 withClue(“Send: ${message}”) { user.sendMessage(message).asClue { it.isSuccess.shouldBeTrue() } } Send: Message(type=MessageType.TEXT, text=”Kotlin”) SendMessageResponse(isSuccess=false, body=”Unauthorized”) expected:<true> but was:<false> Output
  • 23.
    Matcher Modules 様々な形式や Kotlinライブラリ向けの Matcher も用意 ● JSON - io.kotest:kotest-assertions-json ● Ktor - io.kotest.extensions:kotest-assertions-ktor ● kotlinx.time - io.kotest.kotest-assertions-kotlinx-time jsonResponse.shouldEqualJson("""{ "a": true }""") response.shouldHaveHeader(“Server”, “Ktor”) instant.shouldBeBefore(Clock.System.now())
  • 24.
  • 25.
    Property based testing ●ユニットテストちゃんと書けてますか?それテストになってます? ○ エッジケース網羅できてます? ● 決まった入力パターンだけでテストしてませんか? ○ テストパターンを作成するのが面倒くさい val bookA = Book( name = “test book”, category = COMPUTER, pages = 100 ) shouldNotThrowAny { bookService.createBook(bookA) } 決め打ちのパラメータ これ意味ある?
  • 26.
    Property based testing ●checkAll(): 与えられた Generator を基にテストを評価 ● Arb: 任意の無作為な (arbitary) 値を生成する Generator checkAll( iterations = 10, Arb.string(), // ランダム文字列 Arb.<Category>enum().filter { it != UNKNOWN }, // Enum も Arb.int(1..1000) // ランダム整数(エッジケース自動生成) ) { name, category, pages -> val bookA = Book(name, category, pages) shouldNotThrowAny { bookService.createBook(bookA) } } 使いこなすと便利
  • 27.
    Property based testing ●Arb はエッジケースを自動生成する (事が多い) ○ 例: Arb.string(minSize = 0, maxSize = 100) ■ 文字列の長さ (min, max) などが指定された場合、 自動的にそのエッジケースの長さが含まれる ■ エッジケースのテストを別に作る必要がない! ○ 手動でエッジケースを与えることもできる ● Arb は Fail したときに自動で Shrinking する (場合がある) ○ 文字列長が max では通らない場合、少しづつ減らして確認 ○ 邪魔になることもあるので、無効化することも可能 ● Data driven testing もあるが、Property testing がおすすめ
  • 28.
    Arb の例 -Property based testing ● 標準の Arb ○ Arb.string() 文字列 ○ Arb.int(1..100) 1~100 の整数 ○ Arb.uuid() UUID ○ Arb.enum<EnumType>() Enum の値 ○ Arb.domain() ドメイン名 ● Extra arbs - io.kotest.extensions:kotest-property-arbs ○ Arb.firstName() 名前 ○ Arb.wines() ワイン data class(産地・種類・農場) ○ Arb.harryPotterCharacter() ハリーポッターの登場人物
  • 29.
  • 30.
    Coroutines in JUnit5 ● `runTest` 使ってますか? ○ え、`runBlocking` 使ってるんですか? ○ 使い分けが重要: runTest は delay を無視してくれたり ● なんか面倒ですよね? @Test suspend fun testA() { ... } // Error @Test fun testA() = runTest { ... } // OK
  • 31.
  • 32.
    Kotest の TestFramework ● 以下の例は StringSpec ○ 他にもいくつかのスタイル (Spec) が用意されている ○ JUnit 風味の Spec もある (AnnotationSpec) ● テキストでテスト名を書くので、テスト結果が分かりやすい! class UserServiceTest : StringSpec({ “User can’t send text message to blocked user” { user.sendMessage(...) // coroutines ... } }) テストメソッドは 標準で suspend fun StringSpec: テスト名を文字列で管理
  • 33.
    テストケース、構造化してますか? class UserServiceTest { @Nested classUserServiceMessageTest { @Test suspend fun sendMessageTest() { .. } } @Nested class UserServiceFriendTest { @Test suspend fun followTest() { .. } } } JUnit でも @Nested で きるけど...
  • 34.
    Test Framework -構造化テスト ● ShouldSpec ○ should から始まるテスト名を強制できる ○ 他にも構造化テストケースに対応した Spec が多数 class UserServiceTest : ShouldSpec({ context(“send message”) { should(“not send message to blocked user”) { user.sendMessage(...) ... } } }) 何を目的としたテストであるかわかりやすい!
  • 35.
    Test Spec -Test Framework ● FunSpec ● DescribeSpec ● ShouldSpec ● StringSpec ● BehaviorSpec ● FreeSpec ● WordSpec ● FeatureSpec ● ExpectSpec ● AnnotationSpec 沢山ありすぎて、 全部紹介できないよ! ● おすすめ ○ ShouldSpec ○ WordSpec ○ ExpectSpec ○ テストの命名時に、 “何を期待しているか” を強制できる
  • 36.
    Test Framework の導入 ●JVM であれば、JUnit runner を使うのが標準 ○ つまり JUnit の上で kotest の Test Framework が動く ○ 既存の JUnit テストと共存が可能 ● Multiplatform の場合は、kotest-framework-engine を使う ● IntelliJ Plugin もあるので入れておきましょう testImplementation('io.kotest:kotest-runner-junit5:$version') test { useJUnitPlatform() }
  • 37.
    Kotest の TestFramework ● 柔軟なテストケースの生成 ○ テストの名前の自由度 ○ 構造化したテストケース ○ 動的なテストケースの生成 ○ テストケースごとの詳細な設定 (例: timeout, enabledIf) ● Coroutines 対応 ○ test method が標準で suspend fun ● 並列テストの設定 ○ Coroutines で実行するため JUnit より柔軟 & 高速に ● など、様々なメリット...
  • 38.
    その他 - TestFramework ● Mocking ○ mockk や mockito-kotlin を使ってください ○ ただし、標準では Spec 全体が Singleton のため注意 ■ mock のリセットが必要 ■ afterTest { clearMocks(repository) } ● @SpringBootTest - io.kotest.extensions:kotest-extensions-spring ○ Bean の constructor injection ができる (さよなら lateinit var) @SpringBootTest class ControllerUnitTest( @MockkBean private val service: SomeService, ) : StringSpec({
  • 39.
    Kotest を使ってみた感想 何だかんだ5年以上 Kotestを業務で使って見た感想 (kotlintest 時代から) ● 最近はとても安定している (以前は...) ● サービスの網羅的な E2E テスト・外形監視にも利用した ○ ユニットテストは違う観点が必要、豊富な機能が役に立つ ● チームメンバーからの評価も上々 一方で... ● Kotlin のバージョンアップで壊れることがある ○ kotest はすぐ Kotlin のバージョンに追従する && 新機能を使う ○ Kotlin のバージョンアップが必須な事が多い ● Test Framework を移行するかは、必要な機能との兼ね合いで判断
  • 40.
    まとめ ● Kotest は素晴らしいテストフレームワークである ○JUnit からも段階的に置き換えられるので、明日から使える ● 強力なアサーションは、テストの信頼性を上げる ○ 人間は難しいことに直面すると手抜きしがち ○ Kotest の豊富な機能で、脆弱なテストをなくす ● Property-based testing を活用すると、さらに堅牢なテストに ○ 固定のテストパターンは無意味 ○ 楽してエッジケースを網羅できる ● Kotest の Test Framework を使って、もっと Kotlin らしく ○ 謎のメソッド名のテストケースを、わかりやすく