Keep on moving

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

Property-Based-Testingをkotlin testで学ぶ

こんにちは、Kotlin初心者です。Property-Based-Testingやってみようとおもったけど、あまり良くわかってなかったのでまとめてみました。

TL;DR

  • KotlinでProperty-Based-Testingやってみた
  • kotestでの流れ

サンプル

今回のサンプルはこちらにあります

github.com

Property-Based-Testing

そもそもProperty-Based-Testingとは

www.kzsuzuki.com

こちらのブログによるとこのブログを元ネタに説明してくれています。

jessitron.com

 記事では、Property-based testingの対照概念を、Example-based testingとしています。  Example-based testingとは、無数にある値から何がしかの基準で入力値を選択し、その入力値でコードを動かして、出力値と事後状態を確認する。つまり、普通に行われているテストですよね。

 Property-based testingでは、一つのテストで数百個の入力に対する結果を検証します。  ここで検証するのは、個々の入力に対するそれぞれの出力そのものではなく、(有効な)入力「群」に対する出力「群」のすべてが、statementで規定された性質(property)を満たしていることです。

jqwik作者の説明

Javaでの pbsツール jqwikの作者がpbsについてまとめてくれています。

jqwik.net

hypothesis.works

によるとFuzzingテストとpbsの違いは以下のような感じらしいです。

The main reason I’m drawing it here is that they do feel like they have a different character - property based testing requires you to reason about how your program should behave, while fuzzing can just be applied to arbitrary programs with minimal understanding of their behaviour - and also that fuzzing feels somehow more fundamental. [超訳] プロパティベースのテストでは、プログラムの動作を推論する必要がありますが、ファジングは、動作の理解が最小限の任意のプログラムに適用できます,そして、そのファジングは何かもっと根本的なもののようにに感じます。

ファジングテストはpbsとそんなにちがいはないのかもしれないというのがこの話で出てきています。実際に振る舞いをしっていてテストコードをつくるのが大事らしいですね。 プログラムの動作を推論というのは ^ の話でのstatement(property?)の話とほぼ同義の話をしているようにも思えます。

propertyを、「さまざまなデータポイントにおいて維持されるべき、ハイレベルな振る舞いの仕様」としています。

Property based testing is the construction of tests such that, when these tests are fuzzed, failures in the test reveal problems with the system under test that could not have been revealed by direct fuzzing of that system.

Kotlin testでは振る舞いを知っている上で、Example-based testingを補う概念として説明していますね。

GitHub - kotest/kotest: Powerful, elegant and flexible test framework for Kotlin

The problem is it's very easy to miss errors that occur when edge cases or inputs outside of what you considered are fed in. With property testing, hundreds or thousands of values are fed into the same test, and the values are randomly generated by your property test framework. [超訳] 問題は、エッジケースや想定外の入力が入力された場合に発生するエラーを見逃してしまうことです。プロパティテストでは、何百、何千もの値が同じテストに投入され、その値はプロパティテストフレームワークによってランダムに生成されます。

というわけで、これを使ってテストケースを考えてみます。

Kotlin Test

github.com

Kotlin と Kotlin Test(kotest)を使って実際のコードを書いてみます。

install

gradleでの例です。 Property-based-tesitingを使いたいときは kotestに追加で io.kotest:kotest-property-jvm を追加で入れる必要があります。

dependencies {
    testImplementation("io.kotest:kotest-runner-junit5-jvm:version") // for kotest framework
    testImplementation("io.kotest:kotest-assertions-core-jvm:version") // for kotest core jvm assertions
    testImplementation("io.kotest:kotest-property-jvm:version") // for kotest property test
}

こちらを参考に例を作ってみました。

テストの対象とするメソッドは以下のようなものです。

typealias Adder<T> = (T) -> T

object ListUtil {

    fun <T : Number> T.toAdder(): Adder<T> {
        return when (this) {
            is Long -> {
                { it -> (this as Long + it as Long) as T }
            }
            is Int -> {
                { it -> (this as Int + it as Int) as T }
            }
            is Double -> {
                { it -> (this as Double + it as Double) as T }
            }
            else -> throw AssertionError()
        }
    }

    fun <T : Number> sum(zero: T, list: List<T>): T {
        return list.map { it.toAdder() }.fold(zero) { acc, func -> func(acc) }
    }
}

テストコードは以下のようになります。 個々でのproperty(=「さまざまなデータポイントにおいて維持されるべき、ハイレベルな振る舞いの仕様」)は以下の2点です。 * "空のリストの合計は0" * "空ではないリストの合計はtailの合計にheadを足したもの"

class BusinessLogicTests : FunSpec({
    context("ListUtil.sum") {
        test("空のリストの合計は0") {
            ListUtil.sum(0, emptyList()).shouldBe(0)
        }
        test("空ではないリストの合計はtailの合計にheadを足したもの") {
            forAll<Int, List<Int>> { head, tail ->
                ListUtil.sum(0, listOf(head) + tail) == head + ListUtil.sum(0, tail)
            }
        }
    }
})

上の例では、forAllを使用することで、ランダムに生成されたIntとList[Int]の値を受け取ってテストを行っています。forAllに渡したテストの関数が(2, List()), (4, List(1, 4)のようなランダムな値で順番に呼び出されることでテストが実行されるイメージです。

まとめ

僕の理解だと以下のような流れで追加していくのが良さそうです。 1. サンプルベースのテスト 2. それを補完するためにPropertyをみつけ、Property-Based-Testingをする

propertyを、「さまざまなデータポイントにおいて維持されるべき、ハイレベルな振る舞いの仕様」としています。

もうすこし、複雑な例はまた明日書く予定です。 ビジネスロジックのテストに便利そうなので特定のdataclassを生成するときのTipsをまとめる予定です。

関連

github.com

gakuzzzz.github.io

taketoncheir.hatenablog.com

dev.classmethod.jp