テストは実行する前にも数えられるはず
前回 、前々回 と見てきたように、Test Anything Protocolでは本来ひとつひとつのテストに連番が割り振られます。新しいテストを追加したければ、テストファイルの末尾に移動して、テスト番号をひとつずつ増やしながらテストを書き進め、終わったら先頭に戻って宣言部を更新する。先頭に戻るのが面倒であれば宣言部を末尾に移してもよいですが、いずれにしてもテストを追加し終わった時点でテストの件数はわかっているのですから、更新に困ることはないはずでした。
ところが、Perl 5の時代に入ってテスト用のモジュールが連番を振ってくれるようになった結果、テストの件数がわかりづらくなったため、no_planやdone_testingのように実際にテストを実行した回数をテストの総数とみなす手法が登場した――というのが前回の話でしたが、そういった妥協案は、Test Anything Protocolの大事なテストのひとつである「何らかの事情で抜け落ちてしまったテストがないか確認するテスト」を省略することで成り立っているもの。品質を維持するのが大変だからといってテストを削るようでは怠惰さが足りません。
今回はそのようなno_planやdone_testingの欠点をおぎなうために、テストを実行する前にテストの件数を調べる工夫をこらしたテストツールを紹介してみます。
ファイル探索型のテスト
テストの件数を自動的に検出するツールにも何通りかのパターンがありますが、おそらくもっともわかりやすいのはファイル探索型のテストでしょう。PODの書式に誤りがないかを確認するTest::Pod や、公開されている関数、メソッドのドキュメントが不足していないかを確認するTest::Pod::Coverage 、特定の流儀にしたがった書き方をしているか確認するTest::Perl::Critic 、古いPerlをサポートしたいときに、古いバージョンのPerlでは動かない書き方をしていないか調べるTest::MinimumVersion 、コード内にハードタブが混じっていないかを確認するTest::NoTabs 、システムにインストールされているスペルチェッカーを利用して英単語のスペルを確認するTest::Spelling 、モジュールやスクリプトが正しくロードできるか確認するTest::Compile など、特定のディレクトリ配下にあるすべてのモジュールを対象とするタイプのテストモジュールは、いちいちテスト計画を宣言しなくても、テストモジュール側で見つけたファイルの数をテストの件数として宣言してくれるのが特徴です。
使い方はほとんど同じで、たいていの場合はモジュール本体と、必要であればTest::Moreを読み込んで、「 all_..._ok()」のようなテストコマンドを実行するだけ。
use strict ;
use Test :: More ;
use Test :: Pod ;
all_pod_files_ok ();
なかにはあとで直すつもりで「FIXME」のような文字列を入れたまま放置しているモジュールがないか確認するTest::Fixme のように「all_...ok()」以外のコマンドでテストを実行するものや、READMEやMANIFESTが存在しているか、use strictが使われているか、PODにエラーはないかといったディストリビューション全体の品質を確認するTest::Kwalitee のようにロードしただけでテストを実行するものもありますが、これは例外的といってよいでしょう。
use strict ;
use Test :: Fixme ;
run_tests ;
モジュール作者専用のテスト
このようなファイル探索型のテストは、Test::Compileのように特定環境におけるモジュールの動作を確認するものもありますが、ほとんどの場合はどこで実行してもモジュールの動作には影響しない、全体的な品質を担保するものであると考えられます。
そのため、どこで実行しても同じなら、わざわざ多くのユーザにテストしてもらうまでもなく、作者だけが自分の環境で実行しておけば事足りるのではないか、という判断のもと、これらのテストは動作確認用のテストを入れておくt/ディレクトリではなく、作者テスト用のディレクトリとされているxt/ディレクトリに移したり、テストモジュールがインストールされていない場合やリリーステスト以外ではテストをスキップできるようにしたほうがよい、というのが最近のPerlテスト界の論調となっています[1] 。
その際、昔は銘々が勝手にTEST_PODのような環境変数を設定していたものですが、このような変数が増えるとリリーステストのときに指定漏れが出やすくなりますし、自動テストもしづらくなるので、最近ではRELEASE_TESTINGやAUTOMATED_TESTINGといった環境変数に統一しようという話になっていることも頭の片隅に入れておいたほうがよいでしょう。
先ほどのテストであれば、たとえばこのように書き直せます。
use strict ;
use Test :: More ;
eval "use Test::Pod 1.00" ;
plan skip_all => "Test::Pod 1.00 required for testing POD" if $@ ;
plan skip_all => "Author tests" unless $ENV { AUTOMATED_TESTING } or $ENV { RELEASE_TESTING };
all_pod_files_ok ();
ただし、このようなリリーステストの扱いが定められたのは2008年4月にオスロで開催された品質保証チームのハッカソンでのことです。その成果はアダム・ケネディ氏の記事 にまとまっていますが、これはあくまでも小さな分科会での合意にすぎないため、第26回 で紹介したShipIt をはじめとするリリースツール側の対応はまだ完全には終わっていません。テストをスキップさせるのに独自の環境変数を利用している方は上記の変数に移行したほうがよい、ということはいえますが、闇雲に上の書き方を真似してリリース時にテストを忘れるようでは本末転倒です。この種のテストはめったに更新する必要がない分、ほかのモジュールのテストやひな形からコピー&ペーストしたあとは環境変数の設定を忘れてしまいがちなので、環境変数によるスキップなどを有効にする場合は、お使いのリリースツールがその環境変数に対応しているかどうかを確認してからにすることをおすすめします。
[1]
実際にはインストールされている関連モジュールのバージョンなどの影響で微妙に挙動が変わってしまうものもありますし、そもそもそのテスト自体がテストモジュールのテストになっているので、かならずしも作者の環境だけでテストすれば十分というものでもないのですが、ちょっとしたドキュメントのエラーでモジュールのインストールに失敗するのは不便だと考える人が多いのは仕方のないことですし、その結果force installやnotest installがはびこるくらいなら全体的な品質保証のテストは作者まかせにしたほうが無難というのも納得はできます。
データファイルを利用するテスト
ファイル探索型のテストはテスト対象のモジュールが増えてもコードを書き換える必要がない点ではすぐれていますが、バージョン管理ツールのデータや作業中のゴミファイルまで拾ってしまいかねないのが泣き所でした。そのため、これらのテストにはしばしば条件に一致しないファイルをスキップするような仕掛けが用意されているのですが、そのような条件はわざわざテストごとに指定しなくても、たとえばバージョン管理システムの除外ファイルや、MANIFEST.SKIPのようなツールチェーン側で用意している既存のファイルを利用すればDRYに設定できるはずですし、ディストリビューションに含まれるファイル自体、わざわざディレクトリを再帰的に検索していかなくても、ディストリビューションを作成するときに生成されるMANIFESTファイルを使えば必要なファイル情報を得られるはずです。
このように既存のデータファイルを利用して件数を調べるモジュールとしては拙作のTest::UseAllModules などがありますが、この手のデータファイルを利用したテストはむしろ特定のサイトが正しい反応を返しているかを確認する簡単な監視ツールなどで見かけることのほうが多いかもしれません。たとえばこのようなスクリプトを用意してcrontabで定期的に実行しておけば、監視しているサイトに異常が起きているときだけテスト結果をcrontabに登録したメールアドレスに通知することができます[2] 。
use strict ;
use warnings ;
use Test :: More ;
use LWP :: UserAgent ;
use HTTP :: Status 'status_message' ;
use YAML :: Tiny ;
my $conf = shift or die "USAGE: prove $0 :: <config>.yaml > /dev/null" ;
my @urls = @{ YAML :: Tiny :: LoadFile ( $conf ) };
plan tests => scalar @urls ;
my $ua = LWP :: UserAgent -> new ( timeout => 3 );
for my $url ( @urls ) {
my $res = $ua -> head ( $url );
ok $res -> is_success or diag "$url: " . status_message ( $res -> code );
}
[2]
ここでは正常系の出力を/dev/nullに捨てることを前提にしていますが、奥一穂氏作のcronlog のようなツールを使えばproveの正否に応じてもう少し柔軟な運用を行うこともできるでしょう。より本格的な監視を行いたい場合はNagios等の専用ツールを使ってください。
Test::Base
インギー・ドット・ネット氏が2005年の春にリリースしたTest::Base は、このようなテスト対象のデータ化をさらに押し進めたものといえます。
当初はTest::Chunks という名前で公開されていたこのモジュールは、もっとも狭い意味では渡されたデータを別の形に変換して出力するフィルタのようなプログラムをテストするためのものですが、一般的にはもう少し広い意味でとらえて、何らかの引数を与えたら何らかの値が返ってくる、あらゆるタイプの関数やモジュール、アプリケーションのテストに利用されています。
たとえば、特定のサイトに特徴的な文字列が含まれているか確認したいとしましょう。ふつうならURLを渡してコンテンツを取得し、そのコンテンツに対して正規表現がマッチするかどうかを確認する、というテストをループで回していくところですが、Test::Baseを使うと、たとえばこのように書くことができます。
use strict ;
use warnings ;
use Test :: Base ;
use LWP :: Simple ;
plan tests => scalar blocks ;
sub url { get ( $_ ) }
sub regexp { qr /(? six : $_ )/ }
run_like 'input' => 'expected' ;
__DATA__
=== search . cpan . org
--- input url
http :// search . cpan . org /
--- expected regexp
< title > The [ ] CPAN [ ] Search [ ] Site .*</ title >
=== gihyo . jp
--- input url
http :// gihyo . jp / dev / serial / 01 / modern - perl /
--- expected regexp
< title >.+ gihyo \. jp .+</ title >
Test::Baseは裏でソースフィルタが走っていることを含めてさまざまな仕掛けが施されているので通常の書き方とはやや異なる部分もありますが、何をしているかを読みとること自体はそれほどむずかしくはないでしょう。ここでは「__DATA__」以下にある「===」と「---」で区切られたテキストがひとつひとつのテストブロック/データになっていて、search.cpan.orgとgihyo.jpのそれぞれについて、input以下のブロックの内容をurlというフィルタに通した結果が、expected以下のブロックの内容をregexpというフィルタに通した結果得られた正規表現にマッチするかどうかをテストする、という仕組みになっています。
テストブロックの数はblocksというコマンドの返り値を調べればわかるので(実際にはこのコマンドはすべてのブロックを返すのに使われます) 、ここではあえて明示的にスカラー化したものをテストの数として宣言していますが、Test::Baseではplanの行もrun_likeの行も特に必要なければ省略できます(ついでに言うとuse strictとuse warningsの行も省略できますが、筆者は面倒でも省略せずに書いておくほうが好きです) 。
また、ここではテストに直接フィルタとなるコードを書いていますが、コードの内容が複雑だったりほかのテストでも再利用したい場合は、フィルタ用のライブラリを別に用意したほうがよいでしょう。上の例であれば、たとえばこのような内容のMyTest.pmというモジュールを用意して、
use strict ;
use warnings ;
package MyTest ;
use Test :: Base - Base ;
package MyTest :: Filter ;
use Test :: Base :: Filter - base ;
use LWP :: Simple ;
sub url { get ( $_ [ 0 ]) }
sub regexp { qr /(? six : $_ [ 0 ])/ }
1 ;
テストスクリプトからこのように呼び出すと、同じ動作を期待できます(ここではurlやregexpの引数が変わっていることに注意してください。フィルタをテストスクリプトの中に書いた場合は$_を引数にすることができましたが、外部のモジュールに移した場合その書き方ではうまくいきません) 。
use strict ;
use warnings ;
use MyTest ;
__DATA__
=== search . cpan . org
--- input url
http :// search . cpan . org /
--- expected regexp
< title > The [ ] CPAN [ ] Search [ ] Site .*</ title >
=== gihyo . jp
--- input url
http :// gihyo . jp / dev / serial / 01 / modern - perl /
--- expected regexp
< title >.+ gihyo \. jp .+</ title >
より高度な使い方を知りたい方は一世を風靡したPlagger のテストコードがTest::Baseを多用しているので参考にしてみるとよいでしょう。PlaggerのテストではinputブロックにYAMLの設定を渡して、expectedブロックのほうにはよくあるTest::Moreのコマンドを並べる(これらは微調整したうえで最終的にはevalで評価されます)といった書き方もできるようになっています。
さらにテストの再利用性を高めるために
Test::Baseのようなデータ本位のテストは簡単で便利ですし、フィルタ部分を外部モジュールにすることでテストコードの再利用性も高くなっていますが、テストの種類によってはシリアライズしづらい(あるいは余計な手間がかかる)こともあります。たとえば、ひとつの入力に対して複数のテストを行いたい場合(たとえば上のテストで、コンテンツだけでなく、ヘッダやステータスコードのテストも同時に行いたい場合) 、同じコンテンツを何度も取りに行くのは無駄ですし、テストの性質を考えても適切とはいえません(コンテンツの取得はかならずしも成功するとは限りませんし、成功したときのコンテンツ、ヘッダ、ステータスコードの組み合わせと、失敗したときの組み合わせを知りたい場合など、毎回コンテンツを取りに行くのではテストの意図が変わってしまいます) 。
次回はそのような場合にも対応できるフレームワークを見ていきます。