というタイトルで、先日、社内の公開セミナーで話しました。
Haskellのテストフレームワークとベンチマークフレームワークがよくできているので、 これをC++でも使えるんじゃないかという内容です。
概要
背景として、QuickCheck をもっと多くの人に知って/使って貰いたいというのがあります。 QuickCheckは、普段から使っている人間からすると、よくいろいろなバグを拾ってくれるとても便利なものなのですが、 残念ながら普段開発に利用しているC++には相当のもので完成度の高いものが見当たりません。 だからといって、そこから作るためにC++のテンプレートをいじくりまわすには、私はもう老いてしまいました (与えられた関数にランダムな入力を与えるだけなら簡単なのですが、ジェネレータを自由にいじれる機能がやはり欲しいところで)。 そう思った時に、FFIを使えてQuickCheckからC++コードを呼び出せばいいじゃない!、 という事に気づいてやってみたら割りと上手く行ったという次第です。 QuickCheckが素晴らしいならQuickCheckを使えば良かったのです。
技術的説明
FFIを用いてC++のコードを呼び出すと書いていますが、 実はC++のコードをHaskellから呼び出すのは結構大変です (Haskellからに限らずC++のコードを呼び出すのは大変なのですが)。
そこで簡単な解決として、extern "C"
を付けます。 呼び出しが簡単になる代わりに、ポリモーフィックな関数が扱えなくなりますが、 そこは諦めましょう。そもそもテンプレートを他言語からはうまく扱えません。
STLのコンテナやユーザ定義のクラスなどは、直接Haskellとやり取りするのは大変なので、 ポインタを用いるインターフェースのラッパを作っています。
補足
当日の質問や、後で気づいたこと等。
- そもそも、ランダムテストに意味があるのか?
もちろんこれは何かを保証するものではありませんし、 それは普通のテストケースでも同じなのですが、 少ない労力でたくさんのテストデータを生成できるところが良い所です。 QuickCheckでは、(デフォルトでは)徐々に小さい入力から大きな入力を試すようになっているので、 例えば整数だったら最初は0、リストだったら空リストから、小さめのコーナーケースは割りと踏んでくれます。 テストに「ちょい足し」で、大きな効果が得られるというのが利点です。
- テストデータの生成はなんでもできるのか?
なんでも出来ます。数値、文字列、リスト、タプルなどに対してはデフォルトで ジェネレータが用意されています。 それ以外の型でも、Arbitrary クラスのインスタンスにすれば生成できるようになります。
- プロパティを記述することは結局目的のプログラムを書くことではないのか?
関数の適用結果が、仕様を満たしているかどうかを調べることは、 一般的にはその関数を実装することよりも簡単です。 例えば、ソート関数だったら、結果の値が昇順に並んでいて、 かつ同じ要素を含んでいるかどうかを調べればよく、これは簡単に実装できます。
prop_sort xs =
let ys = sort xs in
and (zipWith (<=) ys (drop 1 ys)) && -- すべての隣接要素が昇順
null (xs \\ ys) && null (ys \\ xs) -- 同じ要素を含んでいる
しかし、実際想定解を計算するほうが楽なケースもあります。 同じプログラムを書いていては意味が無いので、少し工夫が必要です。 よくあるのは、「計算量の小さいプログラムの正しさを検証するために、計算量の大きい自明な実装を用いる」 などです。
あともう一つ「トートロジー(恒真式)を利用する」というのがあります。 テストしたい関数に対するトートロジーが簡単に見つかる場合があります。 例えば簡単な例として、リストを逆順にする関数 reverse
に対して、
reverse . reverse == id
のようなものが考えられます。 実際にmsgpackのテストでは、
unpack . pack == id
を利用しています。
- 粒度の大きいルーチンのテストには使いづらいのでは(文字列のパーズ、結果・あるいはエラーレポートが正しいことを調べるとか)?
確かに使いづらいところはあると思います。 QuickCheckでは、比較的小さいパーツをチェックして、 粒度の大きいテストはテストケースを書くといった使い分けは必要でしょう。
JSONパーザのテストなどでは、逆方向の関数(文字列化関数)を利用したテストができます。 まず、JSONのオブジェクトをランダムに生成して(QuickCheckのジェネレータで簡単に書けます)、 それを文字列にしたものをパーズして元のオブジェクトが得られるかをチェックします。
- その他のGenerative Testツール
QuickCheckの亜種として SmallCheck という、 指定した範囲内の値で網羅的にテストをするものもあります。 使い方はQuickCheckに似せてあるので、 これも組み合わせると、僅かな実装コストで更にテストの効果が大きくなると思います。
gencheck という、QuickCheckとSmallCheckを 統一的に扱えるようなライブラリもあるようです。