いわゆるOps(オペレーション)部署にいても、時々運用管理用のユーティリティーを作ることがある。でも共作してるユーティリティーは、リビジョン管理システムではなく共有フォルダに入っているだけで、ドキュメントも試験もなかったりする。そんな環境で、自分の担当ライブラリにドキュメントと試験を埋め込み始めてみた。
ドキュメントと試験を埋め込む。
このユーティリティーはPerlで書いているので、ドキュメントの埋め込み方はPOD(Plain Old Document)が一般的だ。最近のCPANライブラリでは、ドキュメントファイルが別出しでMarkdownで書かれてることもあったりして、僕もMarkdownが大好きだけど、ここはPODで埋め込むことにする。ユーティリティー全体の開発方針をいじるのは大変だから、僕のところで完結する形から始めるんだ。それなら、全体のディレクトリ構成をいじらないで済む、担当する個々のモジュールファイル(.pmファイル)に埋め込む。
同じ理由で、テストも個々のモジュールファイル(.pmファイル)に埋め込む。各モジュールで、試験ライブラリのTest::Moreを読み込んでおき、試験コードを_test
メソッドとして実装しておく。それから、このメソッドを呼び出せば試験できますよ、ということを埋め込みドキュメントに明記しておく。
ちなみに僕が今回作ったのは、圧縮ファイルを7zやtarやunzipコマンドが使えればそれで、なければArchive::Tar
やArchive::Zip
ライブラリで展開するというライブラリなので、OurApp::Extractor
というクラスとそのサブクラスだったことにしよう。まず最初に作ったのは、ログ出力やオブジェクト変数へのアクセサを実装した、OurApp::Extractor::Base
というベースクラスで、こんな感じにしてみた。
package OurApp::Extractor::Base;
use Test::More
sub _test {
my $class = shift;
my $obj;
new_ok($obj, $class);
ok($obj->...)
...
}
(いろんなメソッドの実装)
1;
__END__
=head1 NAME
OurApp::Extractor::Base - OurApp::Extractorのベースクラス
=head1 DESCRIPTION
...
=head1 TEST METHOD
=head2 _test()
このクラスの試験関数です。
コマンドラインからも、以下のように試験を呼び出せます。
perl -MOurApp::Extractor::Base -e "OurApp::Extractor::Base->_test"
=head1 AUTHOR
...
サブクラスでもドキュメントと試験を埋め込む。
次に作ったのは、Tarコマンドを使用するOurApp::Extractor::Tar
クラスだ。OurApp::Extractor::Base
を継承する。
OurApp::Extractor::Tar
のテストでは、親クラスのOurApp::Extractor::Base
から継承したメソッドも試験すべきか少し悩んだけど、「単体試験とは別に、結合試験というのはするものじゃないか」と思って、ここでも試験することにした。この発想があってるのかどうかは自信ないけど。
親クラスのメソッド呼び出しは、SUPER::メソッド名
でできる。でもこれをそのまま呼ぶと、試験の出力結果を見たときに、どこまでが親クラスの試験でどこからがこのクラスの試験か分かりにくいなと思った。そんな時には、Test::More
のsubtest
を使うといいみたいだ。
package OurApp::Extractor::Tar;
use base OurApp::Extractor::Base;
use Test::More;
sub _test {
my $class = shift;
subtest $class->SUPER::_test();
my $obj;
new_ok($obj, $class);
ok($obj->...)
...
}
いつかは1コマンドで各クラス群の試験を片っ端からして呼び出せるようにまとめる日が来ることは予想している。その時には、OurApp::Extractor::Base
の試験は何回も実行されることになる。でもまあいいじゃないか、試験するのは僕じゃなくマシンだし。このテストは一回に数秒もかからないのだし。マシンにやらせる分には、試験項目が多くて片っ端からOKが並んでいくほど見ごたえがあって気持ちいい。
あとドキュメントは同様に埋め込んだし、_test
の呼び出しについても同様に明記した。
...
=head1 TEST METHOD
=head2 _test()
このクラスの試験関数です。
コマンドラインからも、以下のように試験を呼び出せます。
perl -MOurApp::Extractor::Tar -e "OurApp::Extractor::Tar->_test"
=head1 AUTHOR
...
コントローラクラスでもドキュメントと試験を埋め込む。
その後サブクラスが揃ってきたので、展開対象ファイルに応じて、適切なサブクラスで展開を行うコントローラのクラスを作り始めた。いよいよOurApp::Extractor
クラスだ。このクラスはOurApp::Extractor::Tar
とかOurApp::Extractor::PerlLib
とかを読み込む。
上に書いたように富豪的試験方針はもう決めてたので、これらのサブクラスの試験は片端から呼び出すことにした。
package OurApp::Extractor;
use base OurApp::Extractor::Base;
use OurApp::Extractor::Tar;
use OurApp::Extractor::UnZip;
use OurApp::Extractor::7Zip;
use OurApp::Extractor::PerlLib;
use Test::More;
sub _test {
my $class = shift;
subtest $class->SUPER::_test();
subtest OurApp::Extractor::Tar->_test();
subtest OurApp::Extractor::UnZip->_test();
subtest OurApp::Extractor::7Zip->_test();
subtest OurApp::Extractor::PerlLib->_test();
my $obj;
new_ok($obj, $class);
ok($obj->...)
...
}
あとドキュメントは同様に埋め込んだし、_test
の呼び出しについても同様に明記した。
...
=head1 TEST METHOD
=head2 _test()
このクラスの試験関数です。
コマンドラインからも、以下のように試験を呼び出せます。
perl -MOurApp::Extractor -e "OurApp::Extractor->_test"
=head1 AUTHOR
...
サンプルスクリプトでも試験を埋め込む。
コントローラクラスを作るところまでが僕のお仕事なので、これでお仕事終了なのだけど、コントローラクラスを呼び出して実際に書庫ファイルの展開をするサンプルスクリプトextractor.pl
藻作ることにした。OurApp::Extractor
の埋め込みドキュメントには、もちろんSYNOPSYS部に呼び出し方を書いてある。でも本当にその呼び出し方で動くのか、試せるスクリプトがあるといいな、と思ったのだ。
ついでにいうと、それを作るなら、そこにも_test
メソッドならぬ--test
コマンドラインオプションがあったらいい。全部の試験を呼び出すやつ。よろしい、ならば実装だ。
use Test::More;
use OurApp::Extractor;
&test if (grep { /^--test$/ } @ARGV);
&usage if (grep { /^(--help|-h)$/ } @ARGV);
&extract;
exit(0);
(usageとかextractとかの実装)
sub test {
(コマンドライン引数解析とかの試験)
note "OurApp::Extractor & OurApp::Extractor::*";
use_ok('OurApp::Extractor');
my @libs = sort map { s/\//::/g; $_ } grep { s/^(OurApp\/Extractor(?:\/.+)?)\.pm$/$1/ } keys(%INC);
subtest "$_ test" => sub { $_->_test; } foreach (@libs);
done_testing();
exit;
}
ここでは、読み込まれてるクラス群からOurApp::Extractor
とOurApp::Extractor::*
を抽出して、試験対象のクラスにすることにした。OurApp::Extractor
の試験では、明示的にサブクラスの試験呼び出しを1行1行書いてたけど、このクラスはサブクラスが増えたらちゃんとメンテするものだと思ったし、そうしないと読み込ませるの忘れてるサブクラスがあったら試験されないよなと思った。今回のサンプルスクリプトは、OurApp::Extractor
の試験を呼び出せば抜けはないし、サンプルスクリプト自体はそうそう手を入れずに済ませたいと思って、こうした。
これだと、実にOurApp::Extractor
以外全部のクラスの試験が2度以上実行されることになる。とても無駄が多くて、富豪的だ。でもいいじゃないか、贅沢は素敵だ。そろそろ試験がもりもり実行されていく快感に中毒してる頭で、そう思った。実際のところ、OurApp::Extractor
の試験メソッドは将来テスティングツールから頻繁に呼び出されるかもしれないけど、サンプルスクリプトは人手でたまに実行されるぐらいだろう。
Test::Moreだけで一人分だけ試験を始めてみて。
試験を実装すると何が嬉しいのだろう。もちろん品質の証明ができるのはイイことだけど、嬉しいかといわれるとそうでもない。でも実際にやってみると、試験が実装されていると結構毎日、嬉しい僕がいた。
明日の僕が嬉しい。
僕は毎日その日の作業を終わりにして、帰宅していろいろして睡眠して、翌朝また出勤してきて作業を始める。ところが僕の記憶力はなんというか信じられないぐらいダメで、前日の作業状況を思い出せない。たいてい「たしかこの辺まで進んでるんじゃないかなー」とかいって作業を再開するわけだ。
でも試験を実装してみたら、翌朝は試験を実行するところから始めればいいことに気がついた。コードを書きかけで帰っていれば、実行自体がエラーになる。まだ実装が不十分なところがあれば、その部分の試験がエラーになる。どんな状況で前日の作業を終えたかは相変わらず思い出せないのだけど、エラーになったメソッド、だいたい10~50行程度のコードを見直すことから作業を始めれば問題ないわけだ。たいした労力じゃない。
数日後の僕が嬉しい。
クラスを作って数日経つと、そのクラスを上位のクラスで読み込んだり、そのクラスのメソッドを使っていろいろやり始める。ところが僕の記憶力はなんというか信じられないぐらいダメで、呼び出し方とか戻り値とかが、はっきりと思い出せない。通常の呼び出し方と戻り値くらいは思い出せることも多いのだけど、複数の引数の渡し方とか怪しい。エラー時の戻り値とか、そもそも戻り値を返すのかdie
とかしちゃうのかとか、その間にどんなログを出すのだったかといった非正常系と異常系の動作なんか絶対無理。
でも試験を実装してみたら、試験コードを読み直せばいいことに気がついた。ちゃんと呼び出し方も書いてあるし、どんな引数を与えたらどう動くはずかも書いてあるし、戻り値が何になるはずかも書いてある。それに気づいてからは、引数の場合わけをした細かい試験とか、非正常系での戻り値の確認とか、異常系の動作の検証とかする試験を書くのが楽しくなってきた。ログについても、試験を実行してみれば出力が確認できるし。メソッドに分岐を一つ増やしたら、試験も一つ増やす。たいした労力じゃない。
リリース後の僕が嬉しい。
リリース後には、予想が及ばなかった条件で何か問題が起きたりして、しばらくは修正祭りに突入する。バグ0のコードをいきなり作れる精密さはどうやら僕にはないし、カバレッジ100%の試験を用意できる緻密さもどうやら僕にはないらしい。そして残念なことに、修正するときにデグレード(品質低下)とかエンバグ(新しいバグを作っちゃう)を起こさない精緻さ、緻密さもないらしい。端的に言えばたまにやっちゃうし、だからコードの修正に手をつけるのはいつも不安で気が重い。
でも試験を実装してみたら、まずその問題を検出できる試験の追加から始めればいいことに気がついた。動作部分のコードを変更すると、何かやらかしちゃうことがあるけど、試験コードを追加するだけならデグレードもエンバグも無縁だ。これだったら、気軽にひょいひょい手をつけられる。それから動作部分を修正して、テストを実行して、通ったら修正完了。テスト・ファーストなんてめんどくさくて気が塞ぐとか思ってたけど、テスト・ファーストの理由はまさかのその逆、その方が気軽だからだったりした。
スモールスタートでも自動試験は嬉しい。
本気のDevやDevOps環境だと、テスト駆動開発(TDD)とか継続的インテグレーション(CI)とかxUnit系試験フレームワークとか、なんかものすごい仕組みと規約があって、とりあえず習うより慣れろ、分かるより従え的に試験が組み込まれるのかもしれない。ていうか、試験に僕が組み込まれるだろう。僕はユニットテストの歯車です。いやそれはそれでよくて、いつかその試験環境をマスターしたら、きっとビッグになれる。
ただここでやったのは、そういう自動試験のビッグバン導入ではなくて、最小限から始めるスモールスタートだった。
- 試験対象は、ユーティリティ全体ではなく、自分で書いた部分だけ。
- 試験対象箇所(コードカバレッジ)は100%ではなく、試験を思いついた分だけ。
- 試験のために覚えることは、例えば「Happy Testing Perl」や「モダンPerlの世界へようこそ」のテスト回(27~30回)の全般ではなく、Test::Moreの基本だけ。
それでも、自動試験を実装するとこの通り結構嬉しい。大半、記憶力が残念な僕だから嬉しいだけに見えるかもしれないけど、多分僕じゃなくても嬉しい。えーと、うん、きっと嬉しい。だからTest::Moreをワンライナーで普段使いするのに慣れたら、次はテストを書くことをお勧めします。