Keep on moving

あんまりまとまってないことを書きますよ

Property-Based-TestingのShrinkingをkotlin testで学ぶ

こんにちは永遠のJVM初心者です。 Property-Based-TestingのShrinkingとはなんぞやがよくわかってなかったのでまとめます。

TL;DR

  • Property-Based-TestingにおけるShrinkingとは

Property-Based-TestingにおけるShrinkingとは

blog.johanneslink.net

jqwikの作者のブログを読んでる感じだと以下の点で重要とのこと

Unless you already have an inkling of what the issue might be, the number itself does not give you an obvious hint. Even the fact that it’s rather large might be a coincidence. At this point you will either add additional logging, introduce assertions or even start up the debugger to get more information about the problem at hand.

言ってしまえばpbtによる確認はrandomにgenerateされたものに過ぎないので、失敗した数値だけでてもよくわかんないよねー。
ロギングを追加したり、アサーションを導入したり、デバッガを起動しないと問題の原因はわからんよね。
なので「最も単純な」例をテスト結果で出してほしいよね。
この探索フェーズは、元のサンプルから開始し、それをだんだん小さくして再度propertyを確認しようとするので、Shrinking(縮小)と呼びます。

ということらしい。

Kotestでの例

こんな感じで超簡単な素数判定を考える。 これにPBTをかけると以下のような結果になる

gist.github.com

ここでは 592298304 でfailになったあと最小の数字までshrinkingしてくれているのがわかる。 最終的に4までShrinkingしてくれるおかげで、あぁなるほど4はそもそも素数じゃないなというのがわかってそもそもこのテスト自体が正しくないことが わかる。

KotestでのShrinkingの実装

現状でのInt型のShrinkerは以下のような実装らしい。

github.com

object IntShrinker : Shrinker<Int> {
   override fun shrink(value: Int): List<Int> =
      when (value) {
         0 -> emptyList()
         1, -1 -> listOf(0)
         else -> {
            val a = listOf(abs(value), value / 3, value / 2, value * 2 / 3)
            val b = (1..5).map { value - it }.reversed().filter { it > 0 }
            (a + b).distinct()
               .filterNot { it == value }
         }
      }
}

うーむコメントが少ないのでlistOfのルールがよくわかんない。 多分 value/3 がfailしたときのルールかな...(^見た感じだとfailしたら3でわって言っている様子が見えるかも) ちょっと自力で実装するにはもう少しコード読まないといけなそう

github.com

  • Returns the "next level" of shrinks for the given value, or empty list if a "base case" has been reached.
  • For example, to shrink an int k we may decide to return k/2 and k-1. とのことなので、なにかいい感じでルールを作るということらしい。ただ、そもそも順序関係が必要だよなぁ...

Shrinkingのルール変更

ShrinkingModeで設定可能

こんな感じ

        checkAll(PropTestConfig(shrinkingMode = ShrinkingMode.Bounded(50)),Arb.int(2..Int.MAX_VALUE)){
            i ->
            isPrimeNumber(i) shouldBe true
        }

やめたいときは ShrinkingMode.Offを指定すればいいらしい。

github.com

まとめ

PBTのツールではShrinkingが大事というのはよくドキュメントに書かれていたんだけど、実際に使ってみることで重要性がわかりました。 kotestでは今回の例でつかった intだけでなく long, string, listなどでも使用可能なのでbuild-inの方を組み合わせるだけでも十分テストでつかえそうです。 先人に感謝