いけがみを召喚するには、出現予定を参考にしてください。三週間前までにメールをくだされば、日程を追加するなどしてスケジュールに組み込むことができるかもしれません。勉強会や個人的な会合、中途採用面接などに応じます。
Ruby のランダムテストライブラリ RushCheck を公開している。これは3年前からつくり出したもので、PC で眠っていたものを今年の夏休みに公開したものである。Haskell の QuickCheck を Ruby でも使いたいなあと思ったのがきっかけであった。
ランダムテストというのはテスト手法のひとつである。テストケースに入力するデータをテストインスタンスと言うことにしよう。このとき、テストインスタンスを自動生成するというのがランダムテストの特徴である。たとえば文字列を入力とするテストならば、ランダムテストではその入力文字列をランダムに生成する。いくつもの異なった入力をランダムに生成して、同じテストケースを自動的に何度も実行するというテストの考え方である。
ランダムテストが有効な場合について、Dick Hamlet 氏は When only random testing will doの中で、次の二通りに分類している:
このふたつの場合ではどちらにせよ、一番有効なテストがすぐに決まらない。そこで、テストについて考える時間を費すかわりに、ランダムなテストをたくさん行ってしまって満足してはどうですか、という提案である。
一般に、テストというのは、どれくらいテストに時間をかけるか(設計、準備、テスト回数、テスト時間)と、その結果テストによる幸せ(バグが見付かった、あるいは満足の行く範囲でバグが見付からない)のトレードオフがさっぱりわからないという性格を持っている(わたしは専門家ではないので、知らないことがたくさんあると思う、これは間違っているかもしれない)テストにおけるコスト評価は難しい、というのが私の感想である。なんらかの事情でテストしなければならないときは、できるだけ簡単な方法で、かつ広範囲な入力を試したい、と思うのが当然だろう。
ユニットテストを支援するライブラリ (JUnit など) は、便利な assert 関数や、テスト回数の表示を提供してくれはするが、どのようなテストケースを考えるか、どのようなテストインスタンスを考えるか、については何も支援してくれない(と思う、間違っているだろうか)。その点、ランダムテストはテストインスタンスについて何も考えなくてもよくなるので(代わりにたくさんのランダムな入力を試してくれるから)、テスト実行時のコスト(心理的なものも含めて)を下げてくれる。
Haskell の QuickCheck はランダムテストを提供するだけでなく、テストケースを一階述語論理(もどき)で書くことを支援する。プログラムの性質を記述するために論理の記号(任意の a について○○ならば××)を使えるのは、見やすく組織的にテストケースを書くためには非常に便利である。そんなわけで、Ruby に移植してみようと思った。さいわい、Ruby には block による手続き渡しと Proc による遅延評価があるから QuickCheck のソースコードそのまま (高階関数や遅延、モナド) をほとんどそのままの形で移植することができた。Haskell の記述のままでは効率が悪いところは、手続的なループに書き直したが。
実際にランダムテストを書いてみると、ランダムテストにはメリットがあるような気がしてきた(なので、作ってよかった)。たとえば malformed format string のせいで最近携帯電話がリコールになったようだ(しかも複数の会社で時期をはずして何回も!)。ランダムテストは malformed format string のような、プログラマの想定外かつ境界値的な入力による問題を見付けるのに適していると思う。
ためしに次のようなコードを考えよう。これは sprintf の malformed format string bug を含んでおり、書いてはいけない手のコードである。
def hello(name)
printf("Hello, #{name}\n")
end
printf の第一引数は単なる文字列ではなく、フォーマットを意味する文字列である。フォーマットを指定するの特別な文字(たとえば %s)などが与えられたときは、複数の引数を要求する。この場合だと name に %s が含まれていたら実行時エラー(malformed format string)である。% を含むメールを表示できない携帯電話は printf か sprintf などを間違って使ってしまったのだろうと思う。
テストによって、このようなバグは見付かるのだろうか。プログラマがテストも同時に書いているなら、malformed format string バグを見付けるのは難しいと思う。なぜなら、テストインスタンスに %s を含めるというのは、そもそもテスト実行者が「%s」はあやしい、と考えているからで、そう考えるプログラマはそもそも printf の引数に変数を埋め込むようなことはしないはずだからである。これは間違っていない推論だと思う。このようなバグを埋め込んでしまうプログラマは、printf に関するこの手の危険性について無知だったと思う。
ランダムテストならバグがみつかるだろう、というのはランダムな文字列を何度も試すことにより、「偶然」% の入った文字列も試されるからである。
次のコードは RushCheck を用いた(意図的な)ランダムテストの例であり、テストを繰り返すことによりバグを見付けている。
malformed_format_string =
Assertion.new(String) { |s| sprintf(s); true}
irb> malformed_format_string.check
Falsifiable, after 86 tests:
Unexpected exception: #
... snip error traces ...
["\n&'e!]hr(%&\031Vi\003 }ss"]
false
irb>
ここで強調したいのは、for any random String s, "sprintf(s) and true" というテストケースである。ここには、 %s といった「特殊」な入力はなにも書かれていない。にもかかわらず、86 回目のテストで運良く問題が見付かっている。
私が思うに、プログラマがテストコードも同時に書く場合、プログラマが思い付くテストインスタンスはテストに合格するに「決まっている」。というのは、プログラマはテストに時間を使いたくないので、テストインスタンスの設計を慎重にやらないからである。バグはたいてい入力の境界値に潜むものであるが(これは経験的に)、境界値を追求するのは簡単ではない。
みなさんは、テストファーストだ、ユニットテストだ、と言いながら、実際は「テストに通って当り前」のテストしか書かずに済ませてしまっていないだろうか。そのようなテストはバグを見付けることにはあまり貢献せず、「次の実装の見直しにも合格する」ためのベンチマークにすぎなくなってしまっている(しかもこれまた「当り前に合格する」)。その点、QuickCheck によるランダムテストは、見ためが単純かつ明解で、かつ「テストに通るのは必ずしも当り前ではない」ようなテストケースをすばやく書け、しかも一回書くだけで異なったテスト入力が 100 回実行される! これは大変便利である。Haskell だけでなく、 Ruby でもランダムテストが役に立つのではないかな、と思う。私の実装はきわめていい加減で、TestUnit との連携もまだうまくないが、そのうち気に入った誰かが書き直してくれるとありがたい。今月〆切とか出張でごたごたしているので、バトンを引き受けてくれる方をお待ちしています。
RSS feed を再開しました。RSS の思想を尊重するために全文配信はしません、あしからず。