テストのためにデーモンを自動的に起動するやりかた2011年版
はじまりはパクり
最近あんまりコード書いてません、lestrratです。
テストを走らせる時にいろんな他のデーモンを立ち上げたり、そのテストのためだけの設定を先にしないといけなかったりとか色々ありますよね。結構長い間Makefile.PLはModule::Installで書いていたせいもあって、ちょっと前にxaicronさんが書いてたModule::Install::TestTargetでごにょごにょやってたのですが、ちょっと前にYappo/tokuhiromさんがproveで書いてたセットアップがまるっと自分の欲しい用途にも使える事に気づいたのでいろんなアプリケーションのテストをそのように変えてみました。
流れ
proveでテストをすると、proveのプラグインを呼び出す設定ができるのですが、これをプラグインというよりテスト前に実行されるフックとして利用する事によって任意の設定用のコードを実行してしまおう、というのが目的です。
プラグインを使う際proveでは-Pスイッチを使って指定します:
# イメージ prove -P MyPlugin1 -P MyPlugin2 ...
ですがこのようにオプションが何個も連なるとどのオプションをどの順番に設定するのかすぐ忘れてしまいますし、プロジェクト内の他の人に実行してもらう時に説明しなければいけなくなってよくないですね。これを回避するために rcファイルを設定します。
rcfileに書いてある内容は全てproveへの引数として適用されますので、rcファイルだけ渡せばOKになります。
prove --rc=/path/to/rcfile
これは簡単!そしてもう--rcさえ入力したくなければ、rcfileをプロジェクトのトップディレクトリに.provercという名前で保存しておけば自動的にそれを読み込んでくれます。
プラグイン
prove -Pでプラグインを指定すると、該当パッケージ内のload() 関数が呼び出されます。
なので例えば以下のようなプラグインを実装しておけばテストを実行した時間を表示することができます:
package MyTest::Timer;
use strict;
sub load {
print STDERR "Starting test at @{[ scalar localtime ]}\n";
}
1;
prove -PMyTest::Timer ... Starting test at Sun Dec 18 16:52:14 2011 ...
もうちょっと工夫すれば、テスト終了時の時刻も表示することができます:
package MyTest::Timer;
use strict;
use Scope::Guard qw(guard);
my $TIMER;
sub load {
print STDERR "Starting test at @{[ scalar localtime ]}\n";
$TIMER = guard {
print STDERR "Ending test at @{[ scalar localtime ]}\n";
};
}
# Global destruction... とか言われないためのハック
END {
undef $TIMER;
}
1;
MySQLを自動的に開始する
ということでもう想像が付くとは思いますが、mysqldを自動的にTest::mysqld経由で開始してみましょう
package MyTest::mysqld;
use strict;
use Test::mysqld;
use Test::More;
my $MYSQLD;
sub load {
if (my $dsn = $ENV{TEST_DSN}) {
diag "TEST_DSN explicitly set. Not starting MySQL";
return;
}
$MYSQLD = Test::mysqld->new(
my_cnf => {
"skip-networking" => ""
}
);
$ENV{TEST_DSN} = $MYSQLD->dsn;
}
END { undef $MYSQLD }
1;
これを仕込んでおきデータベース接続する部分の設定で $ENV{TEST_DSN}を読み込むようにすればこのデータベースを使ってテストができます。必要であればスキーマも流し込んでおくと良いでしょう。
Test::mysqldはインスタンスへのリファレンスをなくしさえすれば自動的にmysqldを停止してくれますので、$MYSQLDをパッケージレベルで保存しておけばテスト中はずっとmysqldは動作しています。ENDは無くても動きますが、なんとなくつけてみました。
というわけでこれを以下のように-Pで呼び出せるようにしておけばmysqldを使うテストもprove経由で簡単に書けますね
prove -PMyTest::mysqld ...
Memcachedもついでに開始
同じようにmemcachedも簡単に開始できます。
package MyTest::memcached;
use strict;
use Test::More;
use Test::TCP;
our @MEMCACHED;
sub load {
diag "Checking for explicit TEST_MEMCACHED_SERVERS";
# do we have an explicit memcached somewhere?
if (my $servers = $ENV{TEST_MEMCACHED_SERVERS}) {
return;
}
my $max = $ENV{TEST_MEMCACHED_COUNT} || 3;
for my $i (1..3) {
push @MEMCACHED, Test::TCP->new(code => sub {
my $port = shift;
diag "Starting memcached $i on 127.0.0.1:$port";
exec "memcached -l 127.0.0.1 -p $port";
});
}
$ENV{TEST_MEMCACHED_SERVERS} = join ",",
map { '127.0.0.1:' . $_->port } @MEMCACHED;
}
END { undef @MEMCACHED }
1;
テスト内では Cache::Memcached/Cache::Memcached::Fast のコンストラクタにmemcachedサーバーの情報を渡せばmemcached混みのテストを書けます。
Cache::Memcached->new({
servers => [ split /, /, $ENV{TEST_MEMCACHED_SERVERS} ]
});
proveからは以下のように呼び出せばよいだけです。
prove -PMyTest::mysqld -PMyTest::memcached ...
Makefile.PL で make testとフック
あとはproveでもmakeでもどっちでも同じように動くよう仕掛けをしておけばよいです。ここでは t/provercというファイルにproveを実行するのに必要な情報を保存してあると仮定します:
# t/proverc # 本当は --lib か --blib を指定してライブラリへのパスを設定する必要があります -PMyTest::memcached -PMyTest::mysqld
ここではプラグイン関連の設定しかしていませんが、各自必要な設定をここにいれておいてください。そしてmake testから何がよばれるのかをフックするには以下のような関数を指定しておけばいいだけです。
# Makefile.PL
sub MY::test_via_harness { "\tprove --rc=t/proverc t\n" }
# Module::InstallだったらWriteAll()
# ExtUtils::MakeMakerだったらWriteMakefile(...)
これで make testを実行した時にproveが実行され、その際にmysqldおよびmemcachedが起動されます。
まとめ
proveのプラグインを使うとPerlのリファレンスカウンティングを使って上記のようなデーモンのライフサイクルをテスト実行時にだけ限定することが可能です。
STFなどのアプリケーションもこの方式に変えました。Jenkinsに入れる時にJUnitやカバレッジ用のプラグインとの連携もこれで楽になりましたのでおすすめですね。
あと個人的には .provercに保存するより、t/provercのようなファイルに入れておく方が
小回りが利いて好きですが、ここは個人的な好みが入ってくるところかもしれません。.provercを使う場合はこのファイルがあると*必ずprovercの中身が適用される*ということを覚えておいてください。例えば mysqldを必要としてないテストだけを実行したい場合などでも.provercを使った場合は強制的にmysqldが起動されますが、他のファイルにしておくといつprovercの内容を適用するのか選べるので自分は好きです。
このやり方をさらに進めていけば、テスト時だけ起動するモックRPCサーバーの起動や、フィクスチャーの設定などもできますので色々試してみるといいかもしれません。
prove on!