アクションをテストできるか?
では,I/Oアクションなどの副作用を持ったコードはQuickCheckでテストできるでしょうか?
残念ながら,副作用を持ったコードをQuickCheckでテストするのは現状では困難です。このため,一般には「副作用のない純粋な関数ではQuickCheckを使い,I/Oアクションのような副作用を伴う純粋でないコードではHUnitを使う」というように使い分けます(参考リンク)。
I/Oアクションを例に理由を考えてみましょう。
第16回で解説したように,I/Oアクションに対するテストは本質的に難しいものです。加えて,QuickCheckを使ったテストでは,以下の二つの事実を両立させなければならないところに難しさがあります。
- I/Oアクションは外部に対して作用を及ぼす
- QuickCheckはI/Oアクションをランダムに生成して実行する
I/Oアクションによる副作用は,コードの内部には存在しません。しかし,この副作用がわからなければ,ランダムに生成したアクションを使ってテストを行うことはできないのです。
一つの解決策は,I/Oアクションの仕様を定義し,その仕様によって引き起こされる状態についてテストすることです。こうした視点に基づいて,IOSpecのようにQuickCheckでテスト可能な仕様を記述したライブラリも作られています。
ただ,I/Oアクションのすべての仕様を列挙するのはとても手間のかかります。そこでQuickCheckの原作者は「仕様とそれと対応する実際のコードの振る舞いをつき合わせてテストする」という機能の提案を行っています(参考リンク1,参考リンク2)。この機能はバージョン2.0で取り入れられる予定です。
これらのライブラリはとても魅力的ですが,すぐにすべてのテストに使えるわけではありません。特にQuickCheckのバージョン2.0は現在開発中であるため,darcsリポジトリから取得してくる必要があります。こうした事情から,I/Oアクションのような副作用を伴うコードに対するテストでは,次善策としてHUnitを使うのが一般的になっているのです。
テストを自動化できるか?
第16回で,HUnitではテストを自動化できると説明しました。では,QuickCheckでテストを自動化することは可能でしょうか。
リファクタリングや機能追加の後,書き換えられたコードが正しいかどうかを調べたい場合などでは,複数の性質を調べるテストを一度に実行できるほうが便利です。もしテストを自動化ができず,必ずGHCiなどのコマンドラインから対話的にテストを行わなければならないとしたら,テストを一度に行いたい場合にとても不便です。
結論から言うと,どんなツールであってもテストの自動化は可能です。個々のテストをI/Oアクションとして書き連ねていけばよいからです。ただ,この方法はとても不格好です。もっとまともな方法はないでしょうか?
現在のQuickCheckでは,こうした目的のためにTest.QuickCheck.Batchモジュールを提供しています。ただ,実際に試してみたところ,様々な不満が残るものでした。そこで,代わりになるツールを二つ紹介したいと思います。
一つは,QuickCheckの原作者が作成したquickCheckというスクリプトです。ただ,残念ながら現在の処理系には付属していません。HackageDBからquickcheck-scriptをダウンロードする必要があります。
ダウンロードしたquickcheck-script-*.tar.gzを解凍すると,quickCheck.hsというファイルがあります。これが今回説明するスクリプトです。
このスクリプトの内容は「ファイル内のprop_接頭辞が付いた関数定義を取り出し,GHCiで順にテストを実行する」というものです。これにより,モジュール内のテストの実行を自動化するのです。
では実行してみましょう。これまでの説明に使ってきたQuickSortモジュール内のテストを自動実行することにします。
$ ./quickCheck.hs QuickSort.hs GHCi, version 6.8.2: http://www.haskell.org/ghc/ :? for help Loading package base ... linking ... done. Prelude> [1 of 1] Compiling QuickSort ( QuickSort.hs, interpreted ) QuickSort.hs:69:0: Warning: No explicit method nor default method for `coarbitrary' In the instance declaration for `Arbitrary Number' Ok, modules loaded: QuickSort. *QuickSort> Loading package old-locale-1.0.0.0 ... linking ... done. Loading package old-time-1.0.0.0 ... linking ... done. Loading package random-1.0.0.0 ... linking ... done. Loading package QuickCheck-1.1.0.0 ... linking ... done. OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. 49% list is short. 17% list is null, list is short. 13% list has only one element, list is short. *QuickSort> <interactive>:1:27: Not in scope: `prop_QuicksortWithClassify'' *QuickSort> *** Exception: stack overflow *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> Arguments exhausted after 8 tests. *QuickSort> Leaving GHCi.
Windowsではスクリプトの最初の行に記述されている#!(shebang)は無視されます(参考リンク1,参考リンク2)。このため,Windowsでは以下のようにrunhaskellコマンドを使ってスクリプトを実行する必要があります。
$ runhaskell quickCheck.hs QuickSort.hs GHCi, version 6.8.2: http://www.haskell.org/ghc/ :? for help Loading package base ... linking ... done. Prelude> [1 of 1] Compiling QuickSort ( QuickSort.hs, interpreted ) QuickSort.hs:69:0: Warning: No explicit method nor default method for `coarbitrary' In the instance declaration for `Arbitrary Number' Ok, modules loaded: QuickSort. *QuickSort> Loading package old-locale-1.0.0.0 ... linking ... done. Loading package old-time-1.0.0.0 ... linking ... done. Loading package random-1.0.0.0 ... linking ... done. Loading package QuickCheck-1.1.0.0 ... linking ... done. OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. 49% list is short. 17% list is null, list is short. 13% list has only one element, list is short. *QuickSort> <interactive>:1:27: Not in scope: `prop_QuicksortWithClassify'' *QuickSort> *** Exception: stack overflow *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> OK, passed 100 tests. *QuickSort> Arguments exhausted after 7 tests. *QuickSort> Leaving GHCi.
何らかの原因でエラーが発生するテストが中にあっても構いません。対話環境での入力を自動化しているだけなので,この例では,コメント化した「prop_QuicksortWithClassify'」の部分で「定義が見つけられない」というエラーが生じています。
ただ,この実行結果からは「どのテストが成功してどのテストが失敗したのか」が全くわかりません。実行したそれぞれのテストの名前を見るにはどうすればよいでしょうか?
実は,こうした目的のために+namesというオプションが用意されています。
$ runhaskell quickCheck.hs +names QuickSort.hs GHCi, version 6.8.2: http://www.haskell.org/ghc/ :? for help Loading package base ... linking ... done. Prelude> [1 of 1] Compiling QuickSort ( QuickSort.hs, interpreted ) QuickSort.hs:56:0: Warning: No explicit method nor default method for `coarbitrary' In the instance declaration for `Arbitrary Number' Ok, modules loaded: QuickSort. *QuickSort> Loading package old-locale-1.0.0.0 ... linking ... done. Loading package old-time-1.0.0.0 ... linking ... done. Loading package random-1.0.0.0 ... linking ... done. Loading package QuickCheck-1.1.0.0 ... linking ... done. prop_Sorted: OK, passed 100 tests. *QuickSort> prop_Quicksort: OK, passed 100 tests. *QuickSort> prop_Quicksort': OK, passed 100 tests. *QuickSort> prop_QuicksortWithClassify: OK, passed 100 tests. 44% list is short. 18% list is null, list is short. 16% list has only one element, list is short. *QuickSort> <interactive>:1:69: Not in scope: `prop_QuicksortWithClassify'' *QuickSort> prop_ComposeAssoc: OK, passed 100 tests. *QuickSort> prop_MapCompose: OK, passed 100 tests. *QuickSort> prop_MapMonad: OK, passed 100 tests. *QuickSort> prop_Ap: OK, passed 100 tests. *QuickSort> prop_AddTwice: Arguments exhausted after 2 tests.
実際に使用された関数名が,テストの成功失敗と合わせて出力されていますね。
また,quickCheck関数の代わりにverboseCheck関数を利用するための+verboseというオプションもあります。
$ runhaskell quickcheck.hs +verbose QuickSort.hs ~ 略 ~ 13: <function> -7 13: <function> -2 13: <function> -5 13: <function> -1 13: <function> 3 13: <function> 0 Arguments exhausted after 13 tests. *QuickSort> Leaving GHCi.
+namesと+versionオプションを組み合わせて使用することも可能です。