Upgrade to Pro — share decks privately, control downloads, hide ads and more …

小さくはじめるPeoperty Based Testing

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for jsoizo jsoizo
November 14, 2025
700

小さくはじめるPeoperty Based Testing

Avatar for jsoizo

jsoizo

November 14, 2025
Tweet

Transcript

  1. 4 CONFIDENTIAL - © 2022 CoDMON Inc. 4 テストを”増やす”ことが正解とも限らない このような障害報告を書いた経験がありませんか?

    ✋ • 原因:   数値の計算ロジックに想定外のバグが混入 • 再発防止策:テストを更にたくさん作る → 正しいのだが、”むやみに” テストを増やすことは   プログラムの 修正コストを 上げてしまうことも ある。   (実装の詳細に踏み込みすぎてしまうなど)
  2. 5 CONFIDENTIAL - © 2022 CoDMON Inc. 5 テストを”増やす”ことが正解とも限らない このような障害報告を書いた経験がありませんか?

    ✋ • 原因:   数値の計算ロジックに想定外のバグが混入 • 再発防止策:テストを更にたくさん作る → 正しいのだが、”むやみに” テストを増やすことは   プログラムの 修正コストを 上げてしまうことも ある。   (実装の詳細に踏み込みすぎてしまうなど) スマートな方法は ないかな???
  3. 6 CONFIDENTIAL - © 2022 CoDMON Inc. 6 知らないことに目を向ける Unknown

    Knowns = 知っていることを しらない (あたりまえ) Unknown Unknowns = 知らないことを しらない (無知) Known Unknowns = 知らないことを しっている (調査すべきこと) Known Knowns = 知っていることを しっている (定着した知識) 知っている(知識がある, 想像できる) 知らない(知識がない, 想像できない) 知っている (意識してる) 知らない (意識してない)
  4. 7 CONFIDENTIAL - © 2022 CoDMON Inc. 7 Unknown Knowns

    = 知っていることを しらない (あたりまえ) Unknown Unknowns = 知らないことを しらない (無知) Known Unknowns = 知らないことを しっている (調査すべきこと) Known Knowns = 知っていることを しっている (定着した知識) 知っている(知識がある, 想像できる) 知らない(知識がない, 想像できない) 知っている (意識してる) 知らない (意識してない) いわゆる入力例を考えてテストするのはここ 知らないことに目を向ける
  5. 8 CONFIDENTIAL - © 2022 CoDMON Inc. 8 Unknown Knowns

    = 知っていることを しらない (あたりまえ) Unknown Unknowns = 知らないことを しらない (無知) Known Unknowns = 知らないことを しっている (調査すべきこと) Known Knowns = 知っていることを しっている (定着した知識) 知っている(知識がある, 想像できる) 知らない(知識がない, 想像できない) しっている (意識してる) しらない (意識してない) 知らないことに目を向ける 理解してないこと=リスクに対して 探索的に向き合う必要がある
  6. 9 CONFIDENTIAL - © 2022 CoDMON Inc. 9 探索し、知っていることの幅を広げる Unknown

    Knowns = 知っていることを しらない (あたりまえ) Unknown Unknowns = 知らないことを しらない (無知) Known Unknowns 知っている(知識がある, 想像できる) しっている (意識してる) しらない (意識してない) Known Knowns = 知っていることを しっている (定着した知識) 知らない(知識がない, 想像できない) 知っていることを 増やすことで リスクを減らせる そのための Property Based Testing
  7. 10 CONFIDENTIAL - © 2022 CoDMON Inc. 10 Property Based

    Testing(PBT)とは?? このような特徴をもっているテスト手法 1. 任意の処理に対しランダムな入力(Arbitrary)を与える 2. 処理の結果が満たす共通の性質(Property)を検証 3. テストが失敗したらよりシンプルな失敗パターンを出す → これにより、探索的にテストができる
  8. 11 CONFIDENTIAL - © 2022 CoDMON Inc. 11 Property Based

    Testing(PBT)とは?? • • • • • • • 入力の集まり • • • • • • • 出力の集まり 処理 性質(Property) すべての出力が 満たすべき条件 • • 任意の入力値を生成(Arbitrary)し、 その値をもって処理を実行した 出力に対する性質(Property)を検証する
  9. 12 CONFIDENTIAL - © 2022 CoDMON Inc. 12 具体例:reverse(list: List<T>)

    • • • • • 入力の集まり • • • • • • 出力の集まり 処理 reverse 性質(Property) ☑ 要素数が入力が同じ ☑ 二回で入力にもどる ☑ 入力同じ要素を持つ • [1,2,3] [100] [7,8,7] [4,6,3] [] [3,2,1] [100] [7,8,7] [3,6,4] [0] [9,99] [99, 9] • []
  10. 13 CONFIDENTIAL - © 2022 CoDMON Inc. 13 テストが失敗したとき 入力(X0~Xn)を徐々に小さくしていく(Shrinking)

    小さく=リストの要素数、Intの値、文字数など 入力の集まり 失敗する入力 • X0 • X0 • X1 • X2 • Xn … 失敗する値の中の最小値 = バグが起きる境界 縮小化 Shrinking • Xn+1 • X1
  11. 14 CONFIDENTIAL - © 2022 CoDMON Inc. 14 PBTを書く方法 •

    JUnit Platformで動作するPBTをサポートしたテストFW ◦ [Java] jqwik ( https://jqwik.net/ ) ◦ [Kotlin] Kotest ( https://kotest.io/ ) • Kotestに限ると ◦ PBTの機能を単体で簡単に利用できる ◦ = テストFWはJUnit 5だがPBT部分だけKotestでも良い ※ 次頁からKotlinによる実装サンプルを出しますが補足説明を入れてます
  12. 15 CONFIDENTIAL - © 2022 CoDMON Inc. 15 サンプル ~テスト対象の関数~

    fun <T> buggyReverse(list: List<T>): List<T> { val result = MutableList<T>() // バグ: index = 0 まで行くべきところを 1 で止めてしまう for (index in list.size - 1 downTo 1) { result.add(list[i]) } return result } 配列の末尾からindex=1まで走査 → index=0の要素が飛ぶ
  13. 16 CONFIDENTIAL - © 2022 CoDMON Inc. 16 サンプル ~テストコード~

    @Test fun `reverse2回で元にもどる`() = runTest { checkAll<List<Int>> { list -> val reversed = buggyReverse(list) assert(buggyReverse(reversed) == list) } }// ※ runTest{} はKotlinの非同期関数を呼び出すためのおまじないです このようにテストを書くことができる
  14. 17 CONFIDENTIAL - © 2022 CoDMON Inc. 17 @Test fun

    `reverse2回で元にもどる`() = runTest { checkAll<List<Int>> { list -> val reversed = buggyReverse(list) assert(buggyReverse(reversed) == list) } } 任意の<List<Int>を生成 =Arbitrary ブロック内を繰り返す サンプル ~テストコード~
  15. 18 CONFIDENTIAL - © 2022 CoDMON Inc. 18 @Test fun

    `reverse2回で元にもどる`() = runTest { checkAll<List<Int>> { list -> val reversed = buggyReverse(list) assert(buggyReverse(reversed) == list) } } 生成されたList<Int>の値で テスト対象の処理を実行し サンプル ~テストコード~
  16. 19 CONFIDENTIAL - © 2022 CoDMON Inc. 19 @Test fun

    `reverse2回で元にもどる`() = runTest { checkAll<List<Int>> { list -> val reversed = buggyReverse(list) assert(buggyReverse(reversed) == list) } } 満たすべき共通の性質を検証 = Property サンプル ~テストコード~
  17. 20 CONFIDENTIAL - © 2022 CoDMON Inc. 20 失敗 →

    Shrinkingのログ Property test failed for inputs 0) [922145860, 862193514, ...and 63 more Attempting to shrink arg [922145860, 862193514, ...and 63 Shrink #1: [922145860] fail Shrink #2: [] pass Shrink result (after 2 shrinks) => [922145860] 1. ランダムな配列で失敗 2. Shrinkingを開始 3. 配列の要素数=1が   最小の失敗とわかる
  18. 21 CONFIDENTIAL - © 2022 CoDMON Inc. 21 Q. ところでProperty見つけるの難しくないですか?

    A. はい!むずかしいです!!!    でも、見つけ方もあります
  19. 22 CONFIDENTIAL - © 2022 CoDMON Inc. 22 Propertyの見つけ方 Property(=処理結果が満たす共通の性質)を見出すパターン

    1. 不変条件に着目する 2. 同じ目的の既存実装を使う 3. 関係 性から 考える 詳細はこの本の3章にあります Fred Hebert 著、山口能迪 訳 『実践プロパティベーステスト ― PropErとErlang/Elixirではじめよう』 (2023年)
  20. 23 CONFIDENTIAL - © 2022 CoDMON Inc. 23 1. 不変条件に着目する  

    例:消費税率の計算 1~100万の範囲内でランダム入力 fun withTax(price: Int): Int = (price * (1 + 0.10)).toInt() checkAll(Arb.int(1..1_000_000)) { price -> // 1円~100万円 val priceWithTax = withTax(price) // Property1: 税込価格は必ず税抜価格以上 assert(priceWithTax >= price) }
  21. 24 CONFIDENTIAL - © 2022 CoDMON Inc. 24 1. 不変条件に着目する  

    例:消費税率の計算 // Property2: 逆算すると元の価格に戻る(誤差1円以内) val reversedPrice = (priceWithTax / (1 + taxRate)).toInt() assert(abs(reversedPrice - price) <= 1) // Property3: 税額は0以上となる assert(priceWithTax − price >= 0)
  22. 25 CONFIDENTIAL - © 2022 CoDMON Inc. 25 2. 同じ目的の既存実装を使う  

    例:JSONエンコーダの置き換え 任意のuser値をランダム入力 checkAll(userArb()) { user -> val oldResult: String = LegacyJsonEncoder.encode(user) val newResult: String = NewJsonEncoder.encode(user) // Property: 新旧の異なるエンコーダから同じ結果が得られる oldResult.shouldEqualJson(newResult) } // ※ shouldEqualJsonはKotestのJSON matcher
  23. 26 CONFIDENTIAL - © 2022 CoDMON Inc. 26 3. 関係

    性から 考える    例:在庫(warehouse)と注文(order)の関係性 checkAll(Arb.list(orderArb())) { orders -> // 注文の配列を生成 val warehouse = Warehouse(initialStock = 100) // 注文数ぶん在庫から引き当てたら orders.forEach { warehouse.ship(it) } // Property: 在庫の残数は初期在庫から注文総数を引いた数と一致 assert(warehouse.remain == 100 − orders.sumOf(it.quantity)) } // ※ sumOfは配列内の要素の特定フィールドの合計を取る
  24. 27 CONFIDENTIAL - © 2022 CoDMON Inc. 27 まとめ   •

    自分の知識の外側を探索的にテストしたほうがよい • Property Based Testing(PBT)はそのための良い手段 • JUnit系のFWであればKotestで簡単にPBTできる • Propertyを見つけるのは難しいが見つけ方もある Property Based Testingを試してみましょう!!
  25. 29 CONFIDENTIAL - © 2022 CoDMON Inc. 29 Appendix: JUnit

    x Kotestするために gradleならbuild.gradle.ktsに依存ライブラリの宣言を追加 Kotlinの非同期関数を呼び出す ためのおまじないが必要 (Coroutineスコープの作成) plugins { // Kotlinを利用するためKotlin Pluginが必要 alias("org.jetbrains.kotlin.jvm:2.2.20") } dependencies { // JUnit5のテストFW, Kotlin非同期処理, Kotest PBT testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") testImplementation("io.kotest:kotest-property-jvm:5.9.1") }
  26. 30 CONFIDENTIAL - © 2022 CoDMON Inc. 30 Appendix: JUnit

    x Kotestするときの注意点 @Test fun `reverse2回で元にもどる`() = runTest { checkAll<List<Int>> { list -> val reversed = buggyReverse(list) assert(buggyReverse(reversed) == list) } } Kotlinの非同期関数を呼び出す ためのおまじないが必要 (Coroutineスコープの作成) JUnitからKotestのPBT機能を呼び出す際にはひと手間必要
  27. 31 CONFIDENTIAL - © 2022 CoDMON Inc. 31 Appendix: KotestのTips

    (1) • デフォルトでいくつかの型用のArbitraryが用意されてる ◦ 境界値が定められており使うことが保証される ▪ Int: 0, 1, -1, Int.MAX_VALUE, Int.MIN_VALUE ▪ List<T>: 空, 1要素, 重複含む複数要素 • その他にもArbitrary用のユーティリティ多いので便利 ◦ Generators List | Kotest
  28. 32 CONFIDENTIAL - © 2022 CoDMON Inc. 32 Appendix: KotestのTips

    (2) • カスタムのArbitraryを作ることも可 ◦ ドメイン型等でテストするときに有用 data class User(val id: UUID, val name: String) // ※ Javaのrecord相当 val userArb: Arb<User> = arbitrary { val id: UUID = Arb.uuid(UUIDVersion.V4).bind() val name: String = Arb.int().map { "User_$it" }.bind() User(id, name) } 各フィールドの型に沿った Arbをもとに対象のデータを作る