決定論的でカスタム可能な(= 再現性と自由度が高い)ランダム値ジェネレータと、それを使った property based testing (以下PBT) のフレームワーク「Testexp」を作りました1。 .NET 6 と .NET Standard 2.1 で動きます。
Property based testing (PBT) とは、テスト対象の関数に具体的な引数を与えてテストを行うのではなく、引数の性質や条件を指定し、そこからランダムに生成された引数を使ってテストを行う手法です。
決定論的とは、(この記事では)同じコードを書いて同じ環境で動かせば同じ結果が得られることです。
サンプルコード
テストは testing
コンピュテーション式を使って記述します。ArgGen.xxxx
でランダムな値を生成し、 test
でテスト関数を実行、 Assert.xxxx
で結果を検証します2。
Testexp は Visual Studio との連携機能などは持たないので、他のユニットテストフレームワーク(下記コードは Xunit)と組み合わせて使います。
module Test
open Testexp
open Xunit
// テスト対象の関数
let plus (x: int) (y: int) = x + y
[<Fact>]
let ``正の数の和はどちらよりも大きい`` () =
testing {
// ランダムな引数を生成
let! x = ArgGen.intRange 1 10000
let! y = ArgGen.intRange 1 10000
// テスト関数を実行
let! result = test plus (x, y)
// 結果を検証
Assert.greaterThan x result
Assert.greaterThan y result
}
このコードで x
, y
それぞれに1~9999のランダムな整数が64回ずつ渡され、64² = 4096 回のテストが行われます。
引数ジェネレータ
Testexp のコンセプトである決定論的でカスタム可能なランダム値を生成する機構が引数ジェネレータです。引数ジェネレータは IArgumentGenerator<'T>
型3の値であり、 ArgGen
モジュールを使って生成します。
PBTにおける「性質や条件からランダムな引数を生成する機能」を担当します。
カスタム可能な引数ジェネレータ
引数ジェネレータは ArgGen
モジュールの関数を使って生成します。範囲内のランダムな数値を生成する ArgGen.intRange
や、ランダムな文字列を生成する ArgGen.string
などのほか、具体的な値を指定する ArgGen.forEach
も用意してあります。
testing {
// 0 ≦ x < 100 のランダムな整数
let! randomInt = ArgGen.intRange 0 100
// 0~9文字のランダムなASCII文字で構成される文字列 (引数も引数ジェネレータ!)
let! randomString = ArgGen.string ArgGen.asciiChar (ArgGen.intRange 0 10)
// もはやランダムではないが、指定値すべてを1回ずつ生成する
let! targeted = ArgGen.forEach [| "りんご"; "バナナ"; "ゴリラ" |]
}
さらに、上記コードの ArgGen.string
のように引数ジェネレータを組み合わせて新たな引数ジェネレータを作ったり、引数ジェネレータをおなじみの map
や filter
といった関数で加工したりできます。例えば、次のようなコードで「奇数の2乗数を生成する引数ジェネレータ」を定義することができます。
let oddSquare min max =
ArgGen.intRange min max
|> ArgGen.filter (fun x -> x % 2 = 1)
|> ArgGen.map (fun x -> x * x)
argGen
コンピュテーション式を使うと、より複雑な引数ジェネレータを作ることもできます。
これらの機能を組み合わせるとかなり自由に引数ジェネレータを作ることができるので、プロジェクト固有の型に対する引数ジェネレータなんかを作って使いまわせるところが嬉しいわけです。
// 例: 決められたフォーマットの顧客IDがあったとして、それを生成する
let customerId =
argGen {
let! upper = ArgGen.intRange 1 10000
and! lower = ArgGen.intRange 1 10000
return $"C-%04d{upper}-%04d{lower}"
}
決定論的な引数ジェネレータ
Testexp の引数ジェネレータは決定論的です。つまり、テストコードを書き換えない限り毎回同じ擬似ランダム値が生成されます。これにはメリットとデメリットがあります。
メリット
- 一度成功したテストは永続的に成功する
- たまに失敗するテストがある場合、ロジック内に確率で発生するバグがあることが確定する
メリットは一言で言えば再現性が高いことです。数多のエンジニアを苦しめてきた「たまにテストが失敗する」現象から解放されます。
特定の引数でのみ発生するバグは毎回テストを失敗させるので「たまに」ではなくなりますし、仮にたまに失敗するテストがある場合、テスト引数は毎回同じなのでロジック側の問題であることが分かります4。
デメリット
- 擬似ランダム値に含まれない引数で発生するバグを見つけられない
逆に毎回同じ引数であるということは直接的なデメリットでもあり、いわゆるテストケース漏れが発生します。うまく再現性との折り合いをつけて解消したいところです。
その他
- 組み込みの
test
関数は、カリー化スタイル・タプルスタイルどちらの関数にも対応しています。 - 組み込みの
test
とAssert.xxxx
を使ってテストを実行した場合、テスト失敗時に引数と戻り値が(例外のメッセージとして)表示されます。