SML版QuickCheck環境を構築する
Haskellには QuickCheck というテストフレームワークが存在します。
これは、テスト対象の関数の入力型が取りえる値をランダムに自動生成し、別に与えた性質(関数)を満たすかどうかをチェックしてくれるライブラリです。
指定された型の取りえる値の中からコーナーケースを突くような入力を生成してくれます*1
QCheck はSMLで実装したML版QuickCheckです。SMLでは一般的に「型に応じた処理」全般が困難ですが、このライブラリでは、あらかじめいくつかの組み込み型に対して用意したジェネレータと、それらを組み合わせるコンビネータによって手動でヒントを与えることで、任意の型のジェネレータを指定出来るようになっています。
SML/NJ 環境にインストール
$ cd /path/to/qcheck $ make -f Makefile.nj $ mkdir -p /path/to/sml/lib/qcheck.cm $ mv .cm /path/to/sml/lib/qcheck.cm/ $ echo 'qcheck.cm qcheck.cm' >> /path/to/sml/lib/pathconfig
付属のテストを実行
$ sml - CM.make "tests.cm";
Basisライブラリの挙動がテストされますが、意外と失敗します(汗
repl上でQCheckを使いたい場合は
- CM.make "$/qcheck.cm";
とすればOK
MLton 環境にインストール
$ make -f Makefile.mlton all
$ ./tests/tests
/path/to/mlton/mlb-path-map に以下のように追記する (cygwinの場合は /usr/lib/mlton/mlb-path-map)
QCHECK_LIB /path/to/qcheck
基本的な使い方
公式チュートリアルの冒頭と同じですが紹介しておきます。
まずテストしたい対象は以下の関数であるとします。
(* 実装した(テストしたい)関数 *) fun succ x = x + 1 fun even x = x mod 2 = 0 fun odd x = x mod 2 = 1
次に満たすべき性質を通常の関数(述語)として記述します。
fun even_xor_odd x = even x <> odd x
関数に与える(つまりQCheckが生成する)入力の型と (可能なら)toStringを指定します。
GenストラクチャにBasisで提供される型と同じ名前のタグが定義されているので、前者はこれを使います。
val int = (Gen.Int.int, SOME Int.toString)
いよいよ関数をテストします。checkGenに型タグとチェックしたい性質を渡します。
val _ = checkGen int ("even<>odd", pred even_xor_odd) (* 実行結果 even<>odd..............ok (100 passed) *)
これだけだと int のあらゆる入力に対しての性質しか記述出来ませんが、
入力を制限したプロパティを記述する演算子(==>)を使えば、より限定的な入力に対して期待する性質を記述することもできます。
infix ==> (* 入力が偶数の時のみ成り立つ性質をチェック *) val _ = checkGen int ("even+1=odd", even ==> odd o succ) (* 実行結果 even+1=odd.............ok (100 passed) *)
サンプルコード
上のコード全体は https://gist.github.com/2931648:Gist に張ってあります。ファイルをダウンロードして同じディレクトリ内で、SML/NJなら
- CM.make "qcheck_sample.cm";
MLtonなら
$ make qcheck_sample.mlb $ ./qcheck_sample
として実行できます。
参考としてSML/NJでの実行結果を載せておきます。
- CM.make "qcheck_sample.cm"; [autoloading] (* 略 *) even<>odd..............ok (100 passed) even+1=odd.............ok (100 passed) odd+1=even.............FAILED (95/100 passed) counter-examples: 1073741823 1073741823 1073741823 1073741823 odd+1=even.............ok (100 passed) [New bindings added.] val it = true : bool
表示の通り、デフォルトでは100パターンが自動的にチェックされます。
出力のチェックが行いやすい場合はユニットテスト(アサート)より便利に使えそうです。
*1:ですよね?