Perl Hackers Hub

第36回Perlのテストモジュールの使い方・作り方-Test::More、Test::Builder、Test::Stream、そしてPerl 6(2)

前回の(1)こちらから。

テストモジュールの作り方

たくさんのテストを書いていくと、もっとアプリケーションの要件に合わせたテスト関数が欲しくなるときがあります。そんなときは、要件に合わせた独自のテストモジュールを作ることで解決できます。今回は、テストモジュールの作り方を解説します。

テスト関数を関数内から呼び出すと期待した動作とならない

実際のテストでは、テストのサンプルコードに書かれているような、1つの関数やメソッドを呼び出して結果を確認するといった単純なものにはなりません。テスト対象のメソッドを動作させるために必要なオブジェクトの生成や、データベースのセットアップ、テスト後のデータの削除など、さまざまな事前処理や事後処理のコードを書く必要があります。これらのコードが増えてくるとテストの見通しが悪くなり、似たようなコードだらけになってしまいます。

そうなってくると、繰り返し出てくるセットアップとテスト関数をまとめて、独自の関数として定義しようとすることになりますが、実はうまくいきません。

たとえばJSONデータに指定したキーが存在することをテストするために、JSON::PPdecode_jsonの呼び出しとis関数をまとめてtest_json_key.tを作ったとしますリスト2⁠。

リスト2 test_json_key.t
1: use Test::More tests => 1;
2: use JSON::PP;
3: sub test_json_key {
4:     my ($json, $key, $val, $name) = @_;
5:     my $ref = decode_json($json);
6:     return is($ref->{$key}, $val, $name);
7: }
8: my $data = q#{ "address": "Gotanda" }#;
9: test_json_key(
10:     $data, 'address',
11:     'Kichijoji',
12:     'address test'
13: );

リスト2を実行すると実際には9行目でテストが失敗がしますが、エラーメッセージにはFailed test 'address test' at test_json_key.t line 6.と出力され、6行目でテストが失敗したと通知してきます。Test::Moreにはisを別の関数でラップしたことを知る手段がないためです。

Test::Builderを使って書きなおす

実はTest::Moreをはじめとしたほとんどのテストモジュールは、Test::Moreに付属するTest::Builderというモジュールを使って作られています。先ほどの関数もTest::Builderを使って書きなおすと正しい結果が得られますリスト3⁠。

リスト3 test_json_key_builder.t
1: use Test::More tests => 1;
2: require Test::Builder;
3: use JSON::PP;
4: sub test_json_key {
5:     my ($json, $key, $val, $name) = @_;
6:     my $ref = decode_json($json);
7:     my $tb = Test::Builder->new;
8:     return $tb->is_eq($json->{$key}, $val, $name);
9: }
10: my $data = q#{ "address": "Gotanda" }#;
11: test_json_key(
12:     $data,
13:     'address',
14:     'Kichijoji',
15:     'address test'
16: );

リスト3の8行目で使用しているis_eqTest::Builderが提供するテストメソッドで、Test::Moreis関数と同じ機能です(実際、Test::Moreis関数の内部では、このis_eqを呼び出しています⁠⁠。

今度はエラーメッセージにFailed test 'address test' at test_json_key_builder.t line 11.と出力され、正しく11行目が通知されます。

このように、定義した関数の中でTest::Builderのメソッドを呼び出すことで独自のテスト関数を作ることができます。また、このように定義した関数をパッケージに入れればオリジナルのテストモジュールの完成です。

テストモジュールの定義
package Test::JSON::Key;
use parent qw/Exporter/;
our @EXPORT = qw/test_json_key/;
sub test_json_key {
...
}

独自のエラーメッセージを表示する

Test::Builderにはokメソッドという、真偽値とテスト名だけを引数に取る、とてもシンプルなテストメソッドが用意されています。テストが失敗すると必ず表示されるFailed [test name] at [test file]line [n].というメッセージは、このメソッドが表示しています。

diagメソッドと組み合わせて次のようにテスト関数を定義すれば、テストが失敗したときに独自のエラーメッセージを表示できます。たいていのテストモジュールではこのokメソッドや、先ほどのis_eqメソッドを内部で呼び出し、独自のテスト関数を定義しています。

テスト結果の判定と、表示
if ($pass) { # テストが成功したとき
    $tb->ok(1, $name);
} else { # テストが失敗したとき
    $tb->ok(0, $name);
    $tb->diag("got: $got");
    $tb->diag("expected: $expected");
}

テストの数を指定する

Test::Moreではテストの数をuse Test::More tests => 42といった形式で指定できますが、独自のテストモジュールでも同じようにテストの数を指定したい場合は、Test::Builderと一緒に提供されているTest::Builder::Moduleを使います。

テスト数の指定
package Test::MyTestModule;
use parent qw/Test::Builder::Module/;
our @EXPORT = qw/my_test/;
sub my_test { ... }

このようにTest::Builder::Moduleを継承すると、次のようにテストコード側でテスト数を指定できますTest::More自体もTest::Builder::Moduleを継承しています⁠⁠。

use Test::MyTestModule tests => 42;

Test::Builder::ModuleExporterモジュールを継承しているので、この場合は@EXPORTで指定したmy_test関数がエクスポートされ、かつテスト数が42となります。

Test::Moreの解説でも触れましたが、テストを書くうえではdone_testingを使って最後にテスト数をカウントする方法がお勧めですが、テストモジュールを作るうえではTest::Builder::Moduleを使って両方のスタイルに対応できるようにしておくのが良いでしょう。

Test::Testerによるテスト結果のテスト

テストモジュールにも当然テストは必要です。テストの結果は標準出力と標準エラー出力にTAP形式で出力されるので先述したTest::Outputを使ってのテストもできますが、Test::Testerというモジュールを使うともっと簡単にテスト結果のテストができます。

Test::Testerは、Perl 5.22.0よりコアモジュール入りしたテストモジュールです。Perl 5.22.0より前のバージョンを使っている場合は、バージョン1.001010以降のTest::MoreをCPANからインストールすると使えるようになります。

Test::TesterではTest::Builderの主要なメソッドを置き換え、テスト結果をオブジェクトとしてテストできるようになっています。たとえばis関数の挙動をテストするテストはリスト4のようになります。

リスト4 test_test_func.t
use Test::Tester;
use Test::More;
check_test(
    sub {
        is(1, 2, "failure test");
        diag("test end");
    },
    {
        ok => undef,
        name => "failure test",
        diag => " got: '1'\n" .
        "     expected: '2'\n" .
        "test end"
    },
    'is test'
);
done_testing;

また、複数のテスト結果をまとめて取得するrun_testsという関数も用意されています。

run_testsによる複数テストの実行
my ($premature, @results) = run_tests(
    sub {
        is(1, 1, "success test");
        is(1, 2, "failure test");
    }
);
is(
    $result[0]->{name},
    "success test",
    "name test"
);

テストが開始される前に出力された文字列は$prematureに入り、各テストの結果が@resultsに入ります。あとはリスト4のcheck_testと同様にokや、namediagといったハッシュキーで結果を取り出し、通常のテスト関数で結果をテストします。

このようにTest::Testerを使えばテスト単位で結果がオブジェクトとして取り出せるため、より構造的に、整理されたテストが書きやすくなります。

<続きの(3)こちら。>

おすすめ記事

記事・ニュース一覧