本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回はmyfinderこと久森達郎さんで、
なお本稿のサンプルコードは、
テストの目的
まずはテストを行う目的を整理します。
コードを壊していないことを確認する
1人で開発するものであれば、
実行・確認を自動化する
テストコードがないと、
自動化されていれば、
Perlアプリケーションのテスト
次に、
Test::Moreによる基本的なテスト
Perlでテストを書くにあたって利用する最も一般的なモジュールは、
Test::Moreはとてもシンプルなライブラリです。次のような単純な消費税率計算をするモジュールを例に説明します。
package ConsumptionTax::JP;
use parent qw/Class::Accessor::Fast/;
__PACKAGE__->mk_accessors(qw/ consumption_tax_rate /);
sub tax_include {
my ($self, $price) = @_;
return $price * (1 + $self->consumption_tax_rate);
}
1;
このモジュールのテストコードは次のようになります。
use Test::More;
use_ok("ConsumptionTax::JP");
my $consumption_jp
= new ConsumptionTax::JP->new({
consumption_tax_rate => 0.05,
});
subtest " メソッド実装チェック" => sub {
#tax_include という関数を実装しているか
can_ok($consumption_jp, 'tax_include');
};
subtest "tax_include の動作チェック" => sub {
my $price = 100;
my $price_in_tax
= $consumption_jp->tax_include($price);
# 期待値と一致しているか
is $price_in_tax, 105, 'match expected';
};
done_testing;
Test::Moreを使ってテストを書いた場合、done_
を記述するのを忘れないようにしてください。このテストコードでは、
- モジュールのuse
- オブジェクトの生成
- メソッドの実装有無
- メソッドが期待した値を返すか
といった基本的な項目を網羅しています。
Test::Moreにはほかにも、
ok($val, "$val is true");
is_deeply($val, { key => 'val'}, "$val is match");
like($val, qr/ 正規表現/, "$val is match");
といったメソッドを備えており、
しかし実際のアプリケーションでは、
Test::Stubによる外部依存テスト
Test::Stubは、
たとえばTest::Stubを用いて、
use Test::More;
use Test::Stub qw/stub/;
use LWP::UserAgent;
use HTTP::Response;
my $ua = LWP::UserAgent->new;
subtest " レスポンス差し替え" => sub {
stub($ua)->get(
HTTP::Response->new(200, "OK")
);
is (
$ua->get('http://example.com/')->code,
200,
'response code is ok'
);
};
done_testing;
といったようにHTTP::Responseオブジェクトを返すだけの内容に差し替えることで、
Test::MockTimeによる時刻のテスト
テストコードで時刻を扱うようなケースでは、localtime
を用いると、
use Test::More;
use Test::MockTime
qw/set_absolute_time set_fixed_time/;
subtest " 時間指定" => sub {
set_absolute_time(0);
# set_absolute_time にsetした直後の時刻
my $abs_time = time;
sleep 1;
# sleep から1 秒経過した直後の時刻
is $abs_time + 1, time, '1 sec past';
};
subtest " 時間固定" => sub {
set_fixed_time(0);
# set_fixed_time にsetした時刻
my $fixed_time = time;
sleep 1;
# set_fixed_time にsetした時刻
is $fixed_time, time, 'fixed time';
};
done_testing;
このように、
Plack::TestによるWebアプリケーションのテスト
Webアプリケーションのテストには、
use Plack::Test;
use Test::More;
use HTTP::Request;
subtest "WebAPP のテスト" => sub {
my $app = sub {
return [
200,
[ 'Content-Type', 'text/plain' ],
[ "Hello" ]
];
};
test_psgi $app, sub {
my $cb = shift;
my $req = HTTP::Request->new(
GET => 'http://localhost/'
);
my $res = $cb->($req);
is $res->code, 200, '200 ok';
is $res->content, 'Hello', 'body ok';
};
};
done_testing;
上記の例はステータスコード200
およびHello
が返されることを期待した処理です。
$app
の部分が実際のWebアプリケーション実装になります。例では$appは単なるcoderefですが、
use Plack::Test;
use Test::More;
use HTTP::Request;
use MyAPP::Foo::Request;
use MyAPP::Foo::Logic;
my $app = sub {
my $env = shift;
$env->{HTTP_COOKIE} = "foo=var";
my $req = MyApp::Foo::Request->new($env);
my $logic = MyApp::Foo::Logic->new(req => $req);
$logic->run;
};
subtest "MyAPP::Foo::Logic のレスポンステスト" => sub {
test_psgi $app, sub {
$cb = shift;
my $res = $cb->(GET => '/path/to/app');
is $res->code, 200;
is $res->content, 'expected response';
}
};
done_testing;
Test::mysqldによるMySQLを利用するテスト
MySQLを利用するテストの場合、
このテストで起動したMySQLは$mysqld
の参照スコープを抜けた段階で自動的に終了処理が行われ、
use DBI;
use Test::More;
use Test::mysqld;
my $mysqld = Test::mysqld->new(
my_cnf => {
'skip-networking' => '',
}
) or plan skip_all => $Test::mysqld::errstr;
subtest "dbh を使うテスト" => sub {
my $dbh = DBI->connect(
$mysqld->dsn(dbname => 'test'));
isa_ok $dbh, 'DBI::db', 'dbh is ok';
# 以降に$dbh を利用するテスト
};
done_testing;
1つのテーブルのみを相手にする場合は上記のような形でもそれほど問題はありませんが、
Test::Memcachedによるmemcachedを利用するテスト
Test::mysqld以外にも、
use Test::More;
use Test::Memcached;
use Cache::Memcached;
my $memd = Test::Memcached->new(
options => { user => 'nobody' }
);
$memd->start;
my $port = $memd->option('tcp_port');
my $client = Cache::Memcached->new({
servers => [ "127.0.0.1:$port" ]
});
subtest "memcached を使うテスト" => sub {
$client->set('key' => 'value');
is $client->get('key'), 'value', 'value is ok';
};
$memd->stop;
done_testing;
memcached のほかにも、
proveコマンドによるテストの実行と結果出力
ここまで、
テストが1つだけならば、
次の例ではtest_
以下にあるテストスクリプトを実行し、
$ prove -r /path/to/test_dir/
/path/to/test_dir/test1.t ................. ok
/path/to/test_dir/test2.t ................. ok
/path/to/test_dir/test3.t ................. ok
All tests successful.
Files=3, Tests=10, 9 wallclock secs ( 0.07 usr 0.02 sys +
5.94 cusr 0.62 csys = 6.65 CPU)
Result: PASS
このコマンドは裏でTAP::Harnessというモジュールを利用しています。
TAP::Harnessのしくみ
Perlのテスト実行を担うモジュールであるTAP::Harnessについて説明します。
TAP::HarnessはPerlのテストフレームワークで、runtests
メソッドにテストファイルの名前を渡すと、
use TAP::Harness;
my $harness = TAP::Harness->new({
verbosity => 1,
lib => [ '/path/to/test_dir/lib', '/path/to/extlib' ],
});
my @tests = (
'/path/to/test_dir/test1.t',
'/path/to/test_dir/test2.t',
'/path/to/test_dir/test3.t',
);
$harness->runtests(shuffle @tests);
このような形でPerlのテストを実行すると、prove
コマンドと同様に、
Failed 0 / 30.
==========
/path/to/test_dir/test1.t .............................
/path/to/test_dir/test2.t ................................
/path/to/test_dir/test3.t ................................
Files=3, Tests=10, 9 wallclock secs ( 0.07 usr 0.02 sys +
5.94 cusr 0.62 csys = 6.65 CPU)
Result: PASS
このTAP::Harnessは後述する高速なCIクラスタの構築における中核モジュールとなるので、
<続きの