こんにちは、Kotlin初心者です。
TL;DR
この記事は前日のこちらの記事の続きです。が、Property-Based-Testing知ってる方はそのまま読んでもらって問題ありません。
いろいろ書いてみた結果をまとめた
- forAll() と checkAll()の使い分け
- generationの回数を増やしたいor減らしたい
- 失敗したときにもう少しテストを流したい
- CustomGenerator
ひとことでいうと
github.com このドキュメント読むといいよ
forAll() と checkAll()の使い分け
ひとことで行ってしまうと関数の定義が違う
- forAll :
(a, ..., n) -> Boolean
- check All :
(a, ..., n) -> Unit
以上。例はこんな感じ
class PropertyExample: StringSpec({ "String size" { forAll<String, String> { a, b -> (a + b).length == a.length + b.length } } "String size2" { checkAll<String, String> { a, b -> (a + b).length shouldBe a.length + b.length } } })
The checkAll approach will consider a test valid if no exceptions were thrown. なので、ちょっと考え方が違うとも言える。 自分はBooleanを返してほしい派なので大体forAllをつかっている。
generationの回数を増やしたいor減らしたい
標準では1000回生成するそう。増やしたいときには以下のようにする。
class PropertyExample: StringSpec({ "some test" { checkAll<Double, Double>(iterations = 10000) { a, b -> // test here } } })
失敗したときにもう少しテストを流したい
デフォルトでは失敗したら即打ち切りなんだけど、もっとテスト失敗パターンを出すことも可能。以下のようにする.
class PropertyExample: StringSpec({ "some flakey test" { forAll<String, String>(PropTestConfig(maxFailure = 3)) { a,b -> // max of 3 inputs can fail } } })
CustomGenerator
Generated values are provided by instances of the sealed class Gen. You can think of a Gen as kind of like an input stream but for property testing. Each Gen will provide a (usually) infinite stream of these values. Kotest has two types of generators - Arb for arbitrary (random) values and Exhaustive for a finite set of values in a closed space.
KotestではGen
To write your own generator for a type T, you just create an instance of Arb
or Exhaustive .
というわけで Arb<T>
or Exhaustive<T>
のインスタンスを実装することで自分好みのGeneratorをつくれる
data class Yen(val amount: Long) test("金額は5000円より高い") { val yenArb = arb { rs -> val nums = Arb.long(LongRange(5001, 10000)).values(rs) nums.map { num -> Yen(num.value) } } forAll<Yen>(yenArb) { yen1 -> yen1.amount > 5000L } }
2変数以上のクラスをGenerateするためのTips
2変数くらいなら zip
でOK.
val userArb = arb { rs -> val names = Arb.string(minSize = 1).values(rs) val ages = Arb.int().values(rs) names.zip(ages).map { (name, age) -> User( name.value, age.value ) } }
3変数以上なら Arb.bind()
がおすすめ
val orderArb: Arb<Order> = Arb.bind( Arb.long(0L), Arb.string(1, 24) ) { a, b -> Order(Item(a, b)) }
別解としてはflatmap+mapでもできたりします(for expression♫)
val expiredCardArb: Arb<CreditCard> = arb { rs -> val nums = Arb.int(0, 9999).values(rs) val holders = Arb.string(minSize = 1).values(rs) val diffs = Arb.long(1, 24).values(rs) nums.flatMap { num -> diffs.flatMap { diff -> holders.map { holder -> val numStr = num.value.toString().padStart(4, '0') CreditCard( number = "${numStr}-${numStr}-${numStr}-${numStr}", limit = today.minusMonths(diff.value), holder = holder.value ) } } } }
とはいえせっかくなので bind
を使ったほうがよさそう。Sequenceで結果返してるのも実行効率考えると良さそう。
// 実装はこんな感じ fun <A, B, T : Any> Arb.Companion.bind(genA: Gen<A>, genB: Gen<B>, bindFn: (A, B) -> T): Arb<T> = arb { val iterA = genA.generate(it).iterator() val iterB = genB.generate(it).iterator() generateSequence { val a = iterA.next() val b = iterB.next() bindFn(a.value, b.value) } }
kotest/generators.md at master · kotest/kotest · GitHub
まとめ
いろいろ書いてみた結果をまとめた
- forAll() と checkAll()の使い分け
- generationの回数を増やしたいor減らしたい
- 失敗したときにもう少しテストを流したい
- CustomGenerator
ドキュメント読んでてscalaCheckなど先に出ているライブラリをちゃんと意識している印象です。 propertyをちゃんと見出すことができれば結構簡単に値をCustomeGenerator使えるのがいい感じです。 ぜひProperty-Based-Testing使ってみると良いと思います。 コードで変なところあったらぜひツッコミをください。