Perl Hackers Hub

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは、Perl関連のイベント ⁠吉祥寺.pm」の主催などで知られるMagnolia.Kさんで、テーマは「テストモジュールの使い方・作り方」です。

本稿のサンプルコードは、WEB+DB PRESS Vol.90のサポートサイトから入手できます。

Perlにおけるテスト

読者のみなさんはテストを書いていますか。

Perlは昔からテストをとても大事にしてきた言語です。テストを書くために必要なテストモジュールはコアモジュールとしてPerl本体と一緒にインストールされていて、すぐにテストが書ける環境が用意されています。CPANにアップされているモジュールには、たいてい十分な量のテストが用意されていて、安心して使うことができます(動くかどうかもわからないモジュールは使えないですよね⁠⁠。

今回はテストモジュールの使い方や、テストモジュールの作り方を通じて、Perlにおけるテストの方法を解説します。また、2015年のクリスマスに正式版がリリースされたPerl 6でのテストの方法についても解説します。

なお、以降のコードは、特に注釈がない限りPerl 5.14.0以降で動作するように書かれており、Perl 5.22.0で動作確認を行いました。

また、紙幅の都合でuse strictと、use warningsはすべて省略していますが、実際にコードを試すときには必ず付けてください。

テストモジュールの使い方

まずはテストモジュールの使い方とテストの実行方法を解説します。

Test::More─⁠─Perlのテストの基本

テストモジュールの中で最も基本的な機能を提供するモジュールがTest::Moreです。Test::MoreはコアモジュールとしてPerl本体と一緒にインストールされています。

Test::Moreを使ったテスト

最初のテストとして、Perlの組込み関数であるlength関数[1]の挙動を確認するlength_test.t[2]というテストを書いてみますリスト1⁠。

リスト1 length_test.t
1: use Test::More;
2: is(length("perl"), 4, "length test");
3: is(length(undef), 0, "undef test");
4: done_testing;

1行目は、テストモジュールのTest::Moreを使用するための宣言です。以降で使用するisという関数が使えるようになります。

2行目は、Test::Moreis関数を使ってテストを実行しています。is関数は引数を3つ取り、1つ目はテストの対象の値(主に関数やメソッドの戻り値⁠⁠、2つ目は期待する値、3つ目はテストの名前を指定します。ここではlength("perl")の戻り値と、期待する値(4)を比較し、テストの名前としてlength testを指定しています。

3行目では再びis関数を使って今度はlength(undef)と、0を比較しています。なお、length(undef)は実際にはundefを返すので、このテストは失敗します(詳細はのちほど説明します⁠⁠。

最後の4行目では、テストがすべて終わったことを宣言するため、Test::Moredone_testing関数を呼び出しています。

テストの実行

リスト1を通常のPerlのプログラムと同じように実行してください。

length_test.tの実行
$ perl length_test.t

ok 1 - length test ―(1)

not ok 2 - undef test ―(2)
#   Failed test 'undef test' 
#   at length_test.t line 3. 
#          got: undef        ―(3)
#     expected: '0'          
1..2  ―(4)
# Looks like you failed 1 test of 2. ―(5)

テストの結果が出力されました。1行ずつ見ていきましょう。

(1)okと出力されていますが、これは1つ目のテストが成功したことを示しています。後ろに続く数字1と文字列は、そのテストが実行された順序を示す連番と、isに渡したテストの名前です。

(2)not okと出力されていますが、これは2つ目のテストが失敗したことを示しています。その後ろはテストが成功したときと同様にテストの連番とテストの名前が表示されています。

(3)では、テストが失敗した原因の詳細が出力されています。最初にテストが失敗した箇所のファイル名と行番号length_test.t line 3が出力され、続けて実際に得られた値gotと期待した値expectedが出力されています。この内容をもとにテストが失敗した原因を解析します。

すべてのテストが終わりdone_testingが実行されると、(4)でそれまで実行したテストの数が1..[テスト数]という形式で出力されます。

失敗したテストがあるとテストプログラムが終了するタイミングで、(5)のように失敗したテストの数が出力されます。

このようにPerlでは、テスト対象と期待する値を比較し、期待どおりならばテストが成功し、期待した内容と異なればテストが失敗する、というスタイルでテストを書いていきます。先ほどのis関数は最も基本的なテストのための関数(テスト関数)ですが、このほかにも目的に合わせてさまざまなテスト関数が用意されています。以降ではよく使うテスト関数と、Test::Moreが提供するそのほかの機能を解説します。

like─⁠─正規表現によるテスト

likeは正規表現を使うテスト関数です。引数の2番目に正規表現へのリファレンスを渡す点がisと異なります。

like(
    "perl-5.22.0.tar.gz",
    qr/perl-5\.\d+\.\d+\.tar\.gz/,
    "archive name test"
);

cmp_ok─⁠─任意の演算子を使った比較テスト

cmp_okは指定した演算子で比較を行うテスト関数です。数値の大小比較など、任意の演算子を使って比較したい場合に使います。

cmp_ok(1 / 10, '<', 0.11, "division test");

is_deeply─⁠─リファレンスのテスト

is_deeplyはリファレンス用のテスト関数です。2つのリファレンスをたどりながら比較を行います。

sub create_person {
    return { name => $_[0], age => $_[1] }
}
is_deeply(
    create_person( "James", "30" ),
    { age => "30", name => "James"},
    "person object test"
);

リファレンスの先に一致しない箇所があるとテストが失敗し、その不一致箇所が出力されます。

plan─⁠─テストの数を指定する

リスト1で用いたdone_tesingはすべてのテストを実行したあとにテストの数をカウントする関数ですが、planという関数で最初にテストの数を指定することもできます。

42個のテストを実行
plan tests => 42;

planを使わずに、Test::Moreを読み込むときに指定することもできます。

# 同じく42個のテストを実行
use Test::More tests => 42;

テストの数と実際に実行したテストの数が不一致の場合、その旨のメッセージが出力されます。

以前はテストの数をあらかじめ指定する方法が主流でしたが、テストを追加したり、削除したりするたびにテストの数を修正するのはミスを誘発しやすいので、現在では最後にdone_testingを呼び出す方法がお勧めです。

subtest─⁠─テストをまとめる

テストをたくさん書いていくと、1つのテストファイルの中のテストの量が多くなりすぎてだんだんと構造が見えにくくなっていきます。理解しやすい単位でテストをグルーピングするためにsubtestというしくみが用意されています。

subtestは、テストの名前とテスト関数を実行するコードリファレンスを受け取ります。

subtest.t
1: use Test::More;
2: subtest 'group1' => sub { is(1, 1);is(1, 1) };
3: subtest 'gourp2' => sub { is(1, 1);is(0, 1) };
4: done_testing;

実行すると、まずコードリファレンスの中身が実行され、テストの結果がTAP形式で出力されます(TAP形式については後述します⁠⁠。

subtestの実行
    # Subtest: group1
    ok 1
    ok 2
    1..2
ok 1 - group1 ―(1)
    # Subtest: gourp2
    ok 1
    not ok 2
    # Failed test at subtest.t line 3.
    # got: '0'
    # expected: '1'
    1..2
    # Looks like you failed 1 test of 2.
not ok 2 - gourp2 ―(2)
#   Failed test 'gourp2'
#   at subtest.t line 3.
1..2
# Looks like you failed 1 test of 2.

先にコードリファレンスの中のテストが実行され、そのテストが全部成功したら(1)のようにsubtest全体が成功となり、1つでもテストが失敗していると(2)のようにsubtest全体が失敗となります。

このようにsubtestはテストのコードを理解しやすいように階層化するために使いますが、使いすぎると今度はテスト結果の出力が読みにくくなってしまうので、あまりに階層が深く、複雑になるようなテストであれば、そもそも複数のファイルにテストを分けたほうがよいでしょう。

diag─⁠─解析用メッセージを出力する

テスト関数が失敗すると原因が出力されますが、独自の解析用の情報を付加したいときがあります(たとえば接続先のデータベースのインスタンス名を出力するなど⁠⁠。そのようなときにはdiag関数を使用します。

is($got, $expcted) or diag("DB:$db, USER:$user");

テストに失敗するとテスト関数自体の戻り値は偽になるので、上記の例ではテストが失敗したときのみDBUSERの情報が出力されます。

diag関数の出力はすべて#が先頭に付加され、標準エラー出力にコメントとして出力されます。

BAIL_OUT─⁠─テストを中断する

BAIL_OUTは、必要なデータベースサーバが立ち上がっていないなど、必要な条件がそろっていないときにテストを中断するために使用します。

BAIL_OUT "abort the test";

BAIL_OUTが呼び出されると、ただちに次のように出力され、テストは中断されます。BAIL_OUT以降のコードはすべて無視されます。

Bail out! abort the test

テスト結果の出力はTAP形式

リスト1で、テストが成功したらok失敗したらnot okが出力されると説明しましたが、これはTAPTest Anything Protocolという規約に基づいています。規約といっても実にシンプルな内容で、次のルールを覚えれば十分です。

  • テストが成功すればok失敗すればnot okを出力する
  • oknot okに続けて、テストの連番とテスト名を出力してもよい(出力しなくてもよい)
  • テストの前か一番最後で、1..[連番]という形式でテストの数を出力する
  • テストが継続できないときはBail out!と出力し、ただちに実行を中断する
  • #で始まる行は、エラー内容の詳細などテスト結果を補足する情報を出力する
  • 上記以外の出力内容は、TAPとしてはすべて無効となる(テストの結果には影響しない)

Perlでは、テスト結果をすべてTAPという規約に従って出力することにより、次に解説するproveコマンドでテスト結果を収集、解析できるようになっています。

なお、TAPに出力される内容に漢字などのUnicode文字列が含まれていると、有名なWide character in print...という警告メッセージが出力されます。これを防ぐためには次のように、Test::Moreを読み込む前に明示的に標準出力や標準エラー出力のエンコーディングを指定します。

use open ':std', ':encoding(utf8)';
use Test::More;

テストを実行し結果を収集するproveコマンド

テストを一つ一つ実行し、TAPの出力を確認し……とやっていると非常に効率が悪いので、Perlにはまとめて複数のテストを実行し、TAP形式のテスト結果をサマライズしてくれるproveというコマンドが用意されています。

リスト1をproveコマンドで実行してみます。

$ prove length_test.t
length_test.t .. 1/?
#   Failed test 'undef's length test'
#   at length_test.t line 3.
#          got: undef
#     expected: '0'
# Looks like you failed 1 test of 2.
length_test.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/2 subtests

Test Summary Report
-------------------
length_test.t (Wstat: 256 Tests: 2 Failed: 1)
  Failed test: 2
  Non-zero exit status: 1
...
Result: FAIL

1つでもテストが失敗すると、TAPの出力から失敗した箇所だけが抜き出されて出力され、どこでテストが失敗したのかがすぐにわかるようになっています。最後にはResult: FAILと表示されるので、最後の1行だけでもテストが失敗したことがわかります。

次に、リスト1の3行目をis(length(undef), undef,"undef test");と書き換え、テストを成功させてみます。

$ prove length_test.t
length_test.t .. ok
All tests successful.
...
Result: PASS

TAPの詳細な出力はすべて省略され、とにかくテストがすべて成功したことだけが簡潔に表示されます。

なお、ここではテストのファイル名を指定しましたが、複数のテストをまとめて実行するときはtというディレクトリにテストを置いておけば、proveのみでまとめて実行してくれます。

次のように明示的にディレクトリの指定もできます。

$ prove testdir/

このようにproveコマンドを使うことで、効率的にテストの実行と結果の確認ができます。

そのほかのテストモジュール

Test::More以外にも、CPANには、例外をテストするTest::Exceptionや、標準出力の結果をテストするTest::Outputなど、さまざまなテストモジュールがアップされています。

Test::Exceptionで例外をテスト
use Test::Exception;
throws_ok { die "exception msg\n" } qr/exception/;
Test::Outputで標準出力をテスト
use Test::Output;
stdout_is sub { print "Hello, World" }, "Hello, World";

テストモジュールを探したいときは、CPANで「Test::」をキーワードに検索してみてください。検索結果が多すぎて、どこから手をつけたらよいかわからないときは、awesome-perlTask::Kensho::Testingなどを参照すると、よく使われている定番のテストモジュールが見つかるでしょう。

ここまでのまとめ

(1)ではテストモジュールの使い方について解説しました。Perlでは、Test::MoreTAPの読み方、proveの3つを覚えればテストを始めることができますので、テストを書くまでの敷居がとても低くなっています。この3つを覚えたら、あとは実際のテストの例をCPANにアップされている数々のモジュールのtディレクトリを見て学んでいくとよいでしょう。

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

おすすめ記事

記事・ニュース一覧