SSブログ
Rehearsal ブログトップ

REPLのセッションをそのまま単体テストにする [Rehearsal]

REPLを持ったプログラミング言語処理系における原始的かつボトムアップな開発手順について考えよう。

まず小さな関数(だとか)を書く。 思った通りにかけているだろうか? そこでREPLを起動してソースをロードし、関数に対して入力を与え、結果を見る。 正常系は問題ない。 そこで異常な入力に対するハンドリングを関数内に追加して、異常処理がうまくいくことを見る。 でもさっきの正常系の処理が変わったりしていないだろうか。 そこでESCに続けてkを何度か押して(またはCtrl+Pを何度か押して)履歴から正常系の入力の式を復元してもう1回確認してみる。 関数の実装がちょっと気に入らないので変更する。 さっきまで動いていた分はまだ正しく動くだろうか。 そこでまた履歴から入力式を復元して叩いてみる。

この試行錯誤の中には「テスト」をしている箇所がある。 でもそれは揮発的だ。 REPLの履歴とプログラマの短期記憶の中だけにあって、しばらくすると消えてしまう。

テスト駆動開発、あるいは単に「単体テストを書く」ことは不揮発なテストによってコードの品質を保とうとする習慣である。 そこではREPLでアドホックにテストをするのではなくてテスティングフレームワークの定める形でテストコードを書く。 テストはことあるごとに自動で繰り返し実行される。 コードの変更が何かを壊していたらすぐにわかるし、いま現在のコードがすべてのテストを通過しているという確証を得ることができる。

では我々はそもそも最初からREPLなんて使うべきではなかったのだろうか。

長期的に見てテスティングフレームワークを使用すべきことは明らかだ。 でもREPLにも良い点はある。敷居の低さだ。 REPL上の作業ではプログラム実行とプログラマの頭の中は実際には 「とにかく入力を与えてみて、結果を見る。その次に結果が妥当かどうかを考え、妥当であればよしとする」 という関係になっている場合が多い。 これはある種の教義を信奉する人にとっては悪い習慣とみなされるだろうが、 ともかく「良い習慣」より敷居が低いことは事実だ。

問題はREPL上の作業とテスティングフレームワークで単体テストを書くこととの間の距離だ。 敷居の低いREPLで行った「テスト」を不揮発にしたくなった段階で、その距離が問題になる。 さっきまでのREPLに打っていた式ではなくて、テスティングフレームワーク上のアサーションを書かなければならなくなるのだ。

Rehearsal は、ここにフィットするテスティングフレームワークだ。 Rehearsalにおける単体テストはREPLのセッションそのものである。 例えばSML/NJでgreet.smlというテスト対象のプログラムを作り、次のようなREPLセッションを行ったとする。

- use "greet.sml";
[opening greet.sml]
val greet = fn : unit -> unit
val it = () : unit
- greet ();
hello!
val it = () : unit

このセッションを不揮発にするには、コピーアンドペーストで次のファイルを作る。

# greet function prints "hello!" to the console
 
```
use "greet.sml";
[opening greet.sml]
val greet = fn : unit -> unit
val it = () : unit
- greet ();
hello!
val it = () : unit
```

これを t/greet.t というファイル名で保存したとすると、テストを再実行するには次のコマンドを打つ。 このコマンドはもちろんMakefileなどに書いておけばよい。

$ rehearsal -command sml -ps1 '- ' -ps2 '= ' t/greet.t
1..1
ok 1 greet function prints "hello!" to the console

RehearsalはREPLがなんであるかは問わない。シェルでもよいしPythonでもScalaでもよい。 出力はPerlのTAPという形式に従っているが、JUnit XML形式の出力ができるのでJenkinsなどに集計させることもできる。

Reharsalのテストスクリプトの文法はMarkdownに合わせている。 だからドキュメントとしてのテストを書いて読みやすい形式に変換することもできるし、 「Markdownで書かれたその言語のチュートリアル」をそのままテストとして実行することもできる。 (例: Ruby, Python

REPLをそのままテストにすることの良い副作用として、プリティプリンタが得られる点がある。 例えばSMLでは(Javaのような言語とは違って)値を文字列に変換する関数は一般的には手に入らず、自分で書かなければならない。 また、SMLのテスティングフレームワークであるSMLUnitでは、 テストが失敗した場合の表示のためにアサートする値に対してプリティプリンタをセットで与えなければならない (期待値と実際の値の比較をエラーメッセージの中に含むために使われる)。 これは合理的ではあるもののREPLと単体テストの間の距離を一層離すことにつながっている。

一方でSMLでもREPLを使うならばそのような面倒くささはない。 REPLは自身のプリティプリンタによって値を表示していて、 プログラマがREPLでテストをするときはREPLが出してくれたものを目で見て妥当性を判断しているからだ。

このようにRehearsalのアプローチはREPLの気軽さを損ねずに自動的な回帰テストの利点を取り入れることができる。 こうした方法が「とにかく自動的な回帰テストを回し始める最初の一歩」を容易にすると考えている。


Rehearsal ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。