LoginSignup
0
0

More than 1 year has passed since last update.

F# 用の Property Based Testing フレームワーク「Testexp」を作った

Posted at

決定論的カスタム可能な(= 再現性自由度が高い)ランダム値ジェネレータと、それを使った 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 のように引数ジェネレータを組み合わせて新たな引数ジェネレータを作ったり、引数ジェネレータをおなじみの mapfilter といった関数で加工したりできます。例えば、次のようなコードで「奇数の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 関数は、カリー化スタイル・タプルスタイルどちらの関数にも対応しています。
  • 組み込みの testAssert.xxxx を使ってテストを実行した場合、テスト失敗時に引数と戻り値が(例外のメッセージとして)表示されます。
  1. F#のPBTフレームワークはFsCheckなどが有名ですが、コードの見た目が好きじゃないことや、決定論的な擬似ランダム値を使いたかったことから自作することにしました。

  2. testAssert の使用は必須ではなく、引数の自動生成機能だけを使うこともできるように設計してあります。

  3. みんな大好きモナド則を満たします。

  4. これが起こるということは純粋関数ではないということです。関数型言語を使うなら重要なロジックほど純粋関数にするのが吉です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0