モダンPerlの世界へようこそ

第27回Test::Most:Test::Moreでは物足りなくなってきたら

Test Anything Protocol

Perlは非常にテストを重視している言語です。連載第14回ではPerl本体のテスト数がどのように推移してきたかを、また連載第24回ではCPANモジュールの品質保証に大きな役割を果たしてきたCPANTSについて簡単に紹介しましたが、Perlとテストのつながりはそれだけではありません。CPANにはTestを名前に含むディストリビューションが500以上もあがっていますし(これは全ディストリビューション数の約2.5%にあたります⁠⁠、Perlで標準的に使われているテスト形式はTest Anything Protocol (TAP)という名前を得て多くの言語に移植され、2008年からはIETFの標準化を目指した活動も始まっています――というと何やらすごいプロトコルのように聞こえるかもしれませんが、Test Anything Protocolというのは要するに最初にテストの個数を宣言し、テストが成功したら「ok (テスト番号)」を、テストが失敗したら「not ok (テスト番号)」を出力する、というだけのもの。だから、理屈がわかっていればprint文だけでも書けてしまいます。

たとえば、テストに使われているperlのバージョンが5.6以上かどうかを確認したいとしましょう。perlのバージョンは$]で取得できるので、このように書いておけばTAP準拠のテストができたことになります。簡単ですね。

#!perl
use strict;
print "1..1\n";
print $] >= 5.006 ? "ok 1\n" : "not ok 1\n";

もっとも、ひとつくらいならこのように手書きしてもかまいませんが、テストの項目数が増えたらとてもこのような書き方はしていられません。そのため、ふつうはモジュールの力を借りるのですが、さてこのとき、みなさんならどう書きますか、というのが今回のお題目。

Test::SimpleとTest::More

おそらく多くの方が真っ先に思いつくのはこのような書き方でしょう。

#!perl
use strict;
use Test::More tests => 1;
ok($] >= 5.006);

上の例とほぼ一対一対応していますから特に説明は不要と思いますが、use Test::MoreでTest::Moreモジュールを呼び出し、そのときにテストの数も指定して、実際のテストはokでくくる。これでテストが通ればokが出力され、テストに失敗すればnot okが出力されるようになるわけですが、残念ながら、これではTest::Moreを十分に使いこなしているとはいえません。これはTest::Moreより単純なTest::Simpleの書き方だからです。

#!perl
use strict;
use Test::Simple tests => 1;
ok($] >= 5.006);

もう少しTest::Moreらしい書き方をすると、こうなります。

#!perl
use strict;
use Test::More tests => 1;
cmp_ok($], '>=', 5.006);

両者の違いはテストに失敗したときの出力にあるのでした。ためしに不等号の向きを反対にして、結果を確認してみましょう。Test::Simple風のokを使ったテストはこのようにテストの正否しかわかりませんが、

> prove test_ok.t
test_ok.t ..
test_ok.t .. 1/1 #   Failed test at test_ok.t line 4.
# Looks like you failed 1 test of 1.
test_ok.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

cmp_okを使うと、いちいちデバッグ用の出力を用意しなくても、失敗したときにはどう失敗したのかがコメントとして出力されます。

> prove test_cmp_ok.t
test_cmp_ok.t ..
test_cmp_ok.t .. 1/1 #   Failed test at test_cmp_ok.t line 4.
#     '5.008009'
#         

もちろんこのような情報はテストの説明文のなかに埋め込むこともできます(この場合はproveコマンドに-vオプションを渡せばテストの結果によらず表示されます⁠⁠。

#!perl
use strict;
use Test::More tests => 1;
ok($] >= 5.006, "Perl version: $]");

また、一般にテスト関数はテストが失敗したときに偽を返すので、このように書いてもよいでしょう(この場合はcmp_okと同じくテストが失敗したときにしか表示されません⁠⁠。

ok($] >= 5.006) or diag "Perl version: $]";

cmp_okの書き方はいささか冗長になるため、つい横着をしてokで書きがちですが、テストがこけるたびにwarnなどを埋め込んで値を調べるくらいなら、最初からcmp_okなどを使う方がスマートです。Test::Moreのコマンドを使えば失敗したテストがどのファイルのどの行にあるか表示されるので、いちいち説明文を書かなくてもそれほど困らない、というのもメリットといえるかもしれません(もちろんテストの意図を明確にするうえでは適切な説明文を書いておいた方が流れを追いやすくなります⁠⁠。

Test::Moreで利用可能なテスト

Test::Moreにはcmp_okのほかにもいくつかのテスト関数が用意されています。たとえば、ふたつの値が一致するかどうかを調べるときはcmp_okよりも簡単なisを、一致しないことを確認するときはisntを使うのでした。このような場合は「,」を使うより「=>」を使う方がいくぶんわかりやすくなる、というのも多くの方がご存じのことでしょう。

is(  $] => 5.006);
isnt($] => 5.006);

正規表現の比較も、likeやunlikeを使うと、失敗したときにどのような正規表現と比較しようとしていたかがわかるので便利です。

like(  $] => qr/5\.006/);
unlike($] => qr/5\.006/);
> prove test_like.t
test_like.t ..
test_like.t .. 1/1 #   Failed test at test_like.t line 4.
#                   '5.008009'
#     doesn't match '(?-xism:5.006)'
# Looks like you failed 1 test of 1.

リファレンスを使った複雑なデータはis_deeplyで再帰的にテストできるのでした。

is_deeply({ foo => 'bar' }, { foo => 'baz' });
> prove test_deeply.t
test_deeply.t ..
test_deeply.t .. 1/1 #   Failed test at test_deeply.t line 4.
#     Structures begin differing at:
#          $got->{foo} = 'bar'
#     $expected->{foo} = 'baz'
# Looks like you failed 1 test of 1.

Test::Moreにはほかにも、モジュールが正しくロードできるか確認するuse_okやrequire_ok、メソッドが利用できるかどうかを確かめるcan_ok、継承関係を確認するisa_okといったテストが用意されています。これまで使ったことがなかった方はぜひ一度Test::MoreのPODをご確認ください。

Test::Most

Test::MoreはPerl 5.6.2からコアモジュール入りしているのですでに多くの方が利用されていると思いますが、本格的にテストを書くようになるとTest::Moreではいささか物足りない部分も出てきます。エラーや警告の出力を確認したいというのは典型的な例ですし、長いテキストや複雑なデータを比較する場合、is_deeplyなどの分析結果では不十分ということも少なくありません。

そのひとつの対策として用意されているのが、Ovidことカーティス・ポー(Curtis Poe)氏によるTest::Mostです。

これは氏が2008年にテストモジュールの利用動向を分析して選んだ利用頻度の高い5つのテストモジュール(Test::More、Test::ExceptionTest::WarnTest::DifferencesTest::Deepをまとめて読み込み、デフォルトでエクスポートされるテスト関数をすべてエクスポートしなおしてくれるというもの。

たとえば、不正な値を入れたらdieしてくれるか確認したい場合は、Test::Exception由来のdies_okでテストできますし、

use strict;
use Test::Most tests => 1;

sub func { die unless defined $_[0] }

dies_ok { func(undef) } 'undef is not allowed';

dieしないことを確認したい場合は同じくTest::Exception由来のlives_okでテストできます。

use strict;
use Test::Most tests => 1;

sub func { die unless defined $_[0] }

lives_ok { func(0) } '0 is acceptable';

入力に問題がある場合に正しく警告が出るか確認したい場合はTest::Warn経由のwarning_likeやwarnings_existを使えばよいでしょう。warnings_existは2番目の例のように複数の警告が出るかもしれない場合に便利です(ここでは「Use of uninitialized value in warn」という警告と「Warning: something's wrong」という警告が出ますが、テストでは「something's wrong」の方のみチェックしています⁠⁠。

use strict;
use warnings;
use Test::Most tests => 2;

warning_like   { '' . undef } qr/uninitialized/;
warnings_exist { warn undef } qr/something's wrong/;

Test::Differences由来のeq_or_diffにはあまりなじみがない方も多いかもしれませんが、テキストメインの比較的簡単なデータ構造を比較したい場合はis_deeplyよりわかりやすい出力が得られます。

use strict;
use Test::Most tests => 1;
eq_or_diff(['foo', 'bar', 'baz'], ['foo', 'baa', 'baz']);
> prove test_diff.t
test_diff.t ..
test_diff.t .. 1/1 #   Failed test at test_diff.t line 4.
# +----+-----+----------+
# | Elt|Got  |Expected  |
# +----+-----+----------+
# |   0|foo  |foo       |
# *   1|bar  |baa       *
# |   2|baz  |baz       |
# +----+-----+----------+
# Looks like you failed 1 test of 1.

さらに柔軟な比較を行いたい場合は、Test::Deep由来のcmp_deeplyを使うと、データ構造の一部のみ無視した比較なども行えるようになります。データベースをなめておかしなデータが含まれていないか確認するようなテストを書きたい場合、Test::Deep由来のヘルパー関数を利用すると非常にすっきりと書くことができます(このテストをもう少し効率よく書く方法については次回とりあげます⁠⁠。

#!perl
use strict;
use Test::Most;
use DBI;

my $dbh = DBI->connect('dbi:SQLite::memory:');
$dbh->do('create table foo (id, pass, status)');
$dbh->do('insert into foo values(?,?,?)', undef, qw/ishigaki *** 1/);
$dbh->do('insert into foo values(?,?,?)', undef, qw/charsbar *** 0/);
my $people = $dbh->selectall_arrayref('select * from foo', {Slice => +{}});

plan tests => scalar @$people;

foreach my $person (@$people) {
    cmp_deeply(
        $person,
        {
            id     => re('^\w+$'), # id
            pass   => ignore(),    # pass
            status => any(0, 1),   # status
        },
    );
}

CPANにはほかにも長文のテキストの比較に特化したTest::LongStringやバイナリデータの比較に特化したTest::BinaryDataあるいはネットワークまわりやCPANのツールチェーンまわりのテストなど、さまざまなテスト用のモジュールが用意されています。Perlが批判される原因となった旧世代のアプリケーションと、いまどきのまともなPerlアプリケーションの違いはテストが用意されているかどうかですので、今回紹介したような基本的なテストツールになじみがなかった方はこの機会にしっかり覚えておいてください。また、今回はあえてとりあげませんでしたが、Test::MoreやTest::Mostにはテストの流れを操作する仕掛けもいくつか用意されています。次回はそのような仕掛けや関連モジュールについてまとめてみる予定です。

おすすめ記事

記事・ニュース一覧