FsCheck


ヒントとコツ

関数の性質

FsCheck はランダムな関数値を生成できるので、関数の性質を検査できます。 例えば、次のように関数合成の結合性を検査できます:

let associativity (x:int) (f:int->float,g:float->char,h:char->int) = ((f >> g) >> h) x = (f >> (g >> h)) x
Check.Quick associativity
No output has been produced.

Tree -> 任意の型 を生成できます。反例が見つかった場合は、関数値が として表示されます。

しかしながら、 FsCheck は Function 型を使用することで、より詳細に生成された関数を表示することが出来ます。 もしそれを使えば、 FsCheck は関数をシュリンクすることさえ可能です。例は以下の通りです:

let mapRec (F (_,f)) (l:list<int>) =
  not l.IsEmpty ==>
      lazy (List.map f l = ((*f <|*) List.head l) :: List.map f (List.tail l))
Check.Quick mapRec
No output has been produced.

Function<'a,'b> 型は呼ばれていたすべての引数、および生成した結果の写像を記録します。 性質では、例のようにパターンマッチによって実際の関数を抽出することが出来ます。 Function は関数を出力し、また関数をシュリンクするために使用されます。

カスタムジェネレータを使うため forAll の代わりにパターンマッチを使用する

既存の型の普通の範囲の値の部分集合、例えばすべての偶数といったものを生成するジェネレータを定義するために、 1ケースだけの共用体を定義して新しい型用のジェネレータを登録すれば、性質をより読みやすくするでしょう:

type EvenInt = EvenInt of int with
  static member op_Explicit(EvenInt i) = i

type ArbitraryModifiers =
    static member EvenInt() = 
        Arb.from<int> 
        |> Arb.filter (fun i -> i % 2 = 0) 
        |> Arb.convert EvenInt int
        
Arb.register<ArbitraryModifiers>()

let ``generated even ints should be even`` (EvenInt i) = i % 2 = 0
Check.Quick ``generated even ints should be even``
No output has been produced.

同様に、今やカスタムシュリンク関数を定義することも簡単です。

FsCheck は NonNegativeIntPositiveIntStringWithoutNullChars などでもこのパターンを使っています。 Arb.Default 型にあるデフォルトの Arbitrary インスタンスを参照してください。

また、この手のジェネレータにとって、 Arb.filterArb.convert そして Arb.mapFilter 関数は役に立つでしょう。

等式の左辺と右辺を出力する等価比較

性質は一般に等価性をチェックします。テストケースが失敗した場合、FsCheck は反例を出力しますが、 特に最初に生成された引数でいくつかの複雑な計算を行う場合は、比較の左辺と右辺も出力すると便利なことがあります。 これを簡単にするために、独自のラベル表示等価コンビネータを定義できます:

let (.=.) left right = left = right |@ sprintf "%A = %A" left right

let testCompare (i:int) (j:int) = 2*i+1  .=. 2*j-1
Check.Quick testCompare
No output has been produced.

もちろん、あなたがよく使用する任意の演算子や関数に対してこれを行うことが出来ます。

FsCheck のテストを実行するためのいくつかの方法

  • あなたのプロジェクトにある fsx ファイルに性質やジェネレータを追加する。実行するのは簡単で、ctrl-a を押してから alt-enter を押すだけで、 結果が F# Interactive に表示されます。ソリューションに組み込まれている dll を参照するときは注意してください。 F# Interactive は セッションの残りの間はずっとそれらをロックし、セッションを終了するまでビルドすることが出来ません。1つの解決策は、dll の代わりにソースファイルを (ソリューションに)含めることですが、それは処理を遅くします。小規模なプロジェクトに有用です。デバッグするのは難しいですが。
  • 別のコンソールアプリケーションを作成する。デバッグは簡単ですし、アセンブリに迷惑なロックを行いません。テストのために FsCheck のみを使用し、 性質が複数のアセンブリをまたぐような場合は、最良の選択肢です。
  • 別のユニットテストフレームワークを使用する。 FsCheck とユニットテストの手法が混在し(いくつかのものはユニットテストを使ってチェックした方が簡単ですし、 逆もまた然りです)、グラフィカルランナーを好む場合に便利です。あなたが使用しているユニットテストフレームワーク次第では、無料で Visual Studio と 上手く統合できるでしょう。このシナリオで FsCheck をカスタマイズする方法は、上記を参照してください。
namespace FsCheck
namespace System
val associativity : x:int -> f:(int -> float) * g:(float -> char) * h:(char -> int) -> bool

Full name: TipsAndTricks.associativity
val x : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val f : (int -> float)
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
val g : (float -> char)
Multiple items
val char : value:'T -> char (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.char

--------------------
type char = Char

Full name: Microsoft.FSharp.Core.char
val h : (char -> int)
val mapRec : 'a -> l:int list -> 'b

Full name: TipsAndTricks.mapRec
val l : int list
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
property List.IsEmpty: bool
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
val head : list:'T list -> 'T

Full name: Microsoft.FSharp.Collections.List.head
val tail : list:'T list -> 'T list

Full name: Microsoft.FSharp.Collections.List.tail
Multiple items
union case EvenInt.EvenInt: int -> EvenInt

--------------------
type EvenInt =
  | EvenInt of int
  static member op_Explicit : EvenInt -> int

Full name: TipsAndTricks.EvenInt
static member EvenInt.op_Explicit : EvenInt -> int

Full name: TipsAndTricks.EvenInt.op_Explicit
val i : int
type ArbitraryModifiers =
  static member EvenInt : unit -> 'a

Full name: TipsAndTricks.ArbitraryModifiers
Multiple items
static member ArbitraryModifiers.EvenInt : unit -> 'a

Full name: TipsAndTricks.ArbitraryModifiers.EvenInt

--------------------
type EvenInt =
  | EvenInt of int
  static member op_Explicit : EvenInt -> int

Full name: TipsAndTricks.EvenInt
val ( generated even ints should be even ) : EvenInt -> bool

Full name: TipsAndTricks.( generated even ints should be even )
val left : 'a (requires equality)
val right : 'a (requires equality)
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
val j : int
Fork me on GitHub