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

Getting_Started_with_Property_Based_Testing.pdf

Avatar for jsoizo jsoizo
November 14, 2025
59

 Getting_Started_with_Property_Based_Testing.pdf

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をもとに対象のデータを作る