Slide 1

Slide 1 text

小さくはじめる Property Based Testing 2025年11月15日 JJUG CCC 2025 Fall 関根 純 @jsoizo

Slide 2

Slide 2 text

2 経歴 インターネット業界でソフトウェアエンジニアとして働いています。 スケーラブルなシステムを作ること、Kotlinプログラミングが好き。 kotlin-csvというPure Kotlinなcsvパーサをメンテしています。 自己紹介 関根 純 せきね じゅん 2023.01 コドモンに開発エンジニアとして入社 2024.01 育児休業(6ヶ月) 様々な副作用と向き合う 2025.01 新規プロダクトや基盤開発に従事

Slide 3

Slide 3 text

3 今日話すこと 探索的テストの必要性 Property Based Testing(PBT)の解説とサンプル Propertyの見つけ方 1 2 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

11 CONFIDENTIAL - © 2022 CoDMON Inc. 11 Property Based Testing(PBT)とは?? ● ● ● ● ● ● ● 入力の集まり ● ● ● ● ● ● ● 出力の集まり 処理 性質(Property) すべての出力が 満たすべき条件 ● ● 任意の入力値を生成(Arbitrary)し、 その値をもって処理を実行した 出力に対する性質(Property)を検証する

Slide 12

Slide 12 text

12 CONFIDENTIAL - © 2022 CoDMON Inc. 12 具体例:reverse(list: List) ● ● ● ● ● 入力の集まり ● ● ● ● ● ● 出力の集まり 処理 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] ● []

Slide 13

Slide 13 text

13 CONFIDENTIAL - © 2022 CoDMON Inc. 13 テストが失敗したとき 入力(X0~Xn)を徐々に小さくしていく(Shrinking) 小さく=リストの要素数、Intの値、文字数など 入力の集まり 失敗する入力 ● X0 ● X0 ● X1 ● X2 ● Xn … 失敗する値の中の最小値 = バグが起きる境界 縮小化 Shrinking ● Xn+1 ● X1

Slide 14

Slide 14 text

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による実装サンプルを出しますが補足説明を入れてます

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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が   最小の失敗とわかる

Slide 21

Slide 21 text

21 CONFIDENTIAL - © 2022 CoDMON Inc. 21 Q. ところでProperty見つけるの難しくないですか? A. はい!むずかしいです!!!    でも、見つけ方もあります

Slide 22

Slide 22 text

22 CONFIDENTIAL - © 2022 CoDMON Inc. 22 Propertyの見つけ方 Property(=処理結果が満たす共通の性質)を見出すパターン 1. 不変条件に着目する 2. 同じ目的の既存実装を使う 3. 関係 性から 考える 詳細はこの本の3章にあります Fred Hebert 著、山口能迪 訳 『実践プロパティベーステスト ― PropErとErlang/Elixirではじめよう』 (2023年)

Slide 23

Slide 23 text

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) }

Slide 24

Slide 24 text

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)

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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は配列内の要素の特定フィールドの合計を取る

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

28 ご清聴ありがとうございました! 🙇 アンケートも回答おねがいします 🙇 全体アンケート セッションアンケート

Slide 29

Slide 29 text

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") }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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 = arbitrary { val id: UUID = Arb.uuid(UUIDVersion.V4).bind() val name: String = Arb.int().map { "User_$it" }.bind() User(id, name) } 各フィールドの型に沿った Arbをもとに対象のデータを作る