FsCheck


モデルベースのテスト

たいていの場合オブジェクトは一連のメソッドによって内部状態がカプセル化されているわけですが、 FsCheck ではこのようなオブジェクトを対象にしたテストを実行することもできます。 FsCheck ではほんの少し手を加えるだけで、テスト対象となるクラスに対してモデルを基準とするような 仕様を定義できます。たとえば以下のような人為的なバグが含まれているクラスがあるとします:

type Counter() =
  let mutable n = 0
  member x.Inc() = n <- n + 1
  member x.Dec() = if n > 2 then n <- n - 2 else n <- n - 1
  member x.Get = n
  member x.Reset() = n <- 0
  override x.ToString() = n.ToString()

このクラスをテストするためのモデルとしては、オブジェクトの内部状態を表すのに役に立ちそうな int 値1つがふさわしいでしょう。 それを踏まえると、以下のような仕様が作成できます:

open FsCheck.Commands

let spec =
  let inc = 
      { new ICommand<Counter,int>() with
          member x.RunActual c = c.Inc(); c
          member x.RunModel m = m + 1
          member x.Post (c,m) = m = c.Get |> Prop.ofTestable
          override x.ToString() = "inc"}
  let dec = 
      { new ICommand<Counter,int>() with
          member x.RunActual c = c.Dec(); c
          member x.RunModel m = m - 1
          member x.Post (c,m) = m = c.Get |> Prop.ofTestable
          override x.ToString() = "dec"}
  { new ISpecification<Counter,int> with
      member x.Initial() = (new Counter(),0)
      member x.GenCommand _ = Gen.elements [inc;dec] }

仕様は ISpecification<'typeUnderTest,'modelType> を実装したオブジェクトです。仕様は、初期状態のオブジェクトと、 そのオブジェクトに対する初期状態のモデルを返さなくてはいけません。また ICommand オブジェクトのジェネレータも返す必要があります。

それぞれの ICommand オブジェクトでは、一般的にはテスト対象のオブジェクトに対する1つのメソッド呼び出しに対応するようにして、 コマンドを実行することでモデルとオブジェクトに対して起こることを定義します。また、コマンドを実行する前に満たすべき事前条件を アサートします。すなわち、もしも前提条件が一致しないのであれば FsCheck はそのコマンドを実行しません。コマンドの実行後は 一致すべき事後条件もチェックされます。すなわち、事後条件が一致しない場合、FsCheck ではテストが失敗したものと判断されます。

なお反例が表示できるように ToString をオーバーライドしておくとよいでしょう。

仕様を以下のようにチェックできます:

Check.Quick (asProperty spec)
No output has been produced.

FsCheck は「バグ」を発見しただけでなく、バグを発生させる最小のシーケンスも生成したことにも注目してください。

namespace FsCheck
namespace System
Multiple items
type Counter =
  new : unit -> Counter
  member Dec : unit -> unit
  member Inc : unit -> unit
  member Reset : unit -> unit
  override ToString : unit -> string
  member Get : int

Full name: StatefulTesting.Counter

--------------------
new : unit -> Counter
val mutable n : int
val x : Counter
member Counter.Inc : unit -> unit

Full name: StatefulTesting.Counter.Inc
member Counter.Dec : unit -> unit

Full name: StatefulTesting.Counter.Dec
member Counter.Get : int

Full name: StatefulTesting.Counter.Get
member Counter.Reset : unit -> unit

Full name: StatefulTesting.Counter.Reset
override Counter.ToString : unit -> string

Full name: StatefulTesting.Counter.ToString
Int32.ToString() : string
Int32.ToString(provider: IFormatProvider) : string
Int32.ToString(format: string) : string
Int32.ToString(format: string, provider: IFormatProvider) : string
val spec : obj

Full name: StatefulTesting.spec
val inc : obj
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 dec : obj
Fork me on GitHub