Perl Hackers Hub

第51回Test2で変わるモダンなテスト―拡張性を持ったテスティングフレームワークとTest2::V0の使い方(3)

(1)こちら⁠2)こちらから。

テスト関数の作り方

テストが大きくなってくると、複数のテスト間で共通して使えるテストの処理を1つの関数としてまとめたくなります。本節ではテスト関数の作成とそのテストの方法について解説します。

まずは、関数の中で通常と同じようにテスト関数を呼ぶようにしてみます。

test-func.t
sub check_numeric_array {
  is $_[0], array {
    all_items match qr/^[0-9]+$/;
    etc;
  }, 'すべての要素が数値である';  ――(1)
}
check_numeric_array([0, '1234']);
check_numeric_array(['a']);  ――(2)

この場合に問題となるのが、呼び出し元をテスト側に反映できないという点です。テストを実行して、失敗したときの出力を見てみます。(2)で失敗すると思われるテストは(1)(5行目)で失敗したことになっており、これではテスト結果がわかりにくいです。

test-func.tの実行結果
$ perl test-func.t
# Seeded srand with seed '20180625' from local date.
ok 1 - すべての要素が数値である
not ok 2 - すべての要素が数値である
# Failed test 'すべての要素が数値である'
# at test-func.t line 5.
# +------+--------------+----+---------------+------+
# | PATH | GOT          | OP | CHECK         | LNs  |
# +------+--------------+----+---------------+------+
# |      | ARRAY(省略)|    | <ARRAY>       | 2, 5 |
# | [1]  | a            | =~ | (?^:^[0-9]+$) | 3    |
# +------+--------------+----+---------------+------+

この問題を解決するためには、isokなどのテスト関数の1つ上の呼び出し元が反映されるように伝える必要があります。Test::Builderの場合は$Test::Builder::Levelというグローバル変数によって管理する必要がありましたが、Test2ではコンテキストと呼ばれるオブジェクトに隠蔽(いんぺい)されています。スコープの変更時にcontext関数を呼び出すことでコンテキストを作り、抜けるときに作成したコンテキストのreleaseメソッドを呼び出します。こうすることで、テスト関数の呼び出し元を正しく反映できます。

sub check_numeric_array {
  my $ctx = context;
  is $_[0], array {
    all_items match qr/^[0-9]+$/;
    etc;
  }, 'すべての要素が数値である';
  $ctx->release;
}

テスト関数のテスト

テスト関数を書いたなら、その関数が正しいかどうかテストを書きたくなるはずです。Test2では、テストの結果をイベントと呼ばれる単位で取り出すことで、容易にテスト関数のテストができるようになっています。イベントはあるテストが成功したことや出力されたといった単位を表し、表4のようにいくつかの種類があります。

表4 イベントの種類
イベント名アクセサ説明
Okpass、effective_passokやis関数によるテストの結果が成功した場合はpassが1となり、todo関数によりテスト結果を無視する場合はeffective_passが1となる
Diagmessagediag関数によってメッセージが出力された
Notemessagenote関数によってメッセージが出力された
Skipreasonskip関数によりテストがスキップされた
Subtestpass、effective_pass、subeventssubtest関数によりsubtest化されたテスト、subtest内で発行されたイベントはsubeventsに含まれる

イベントの取り出し

イベントの取り出しには、intercept関数を使います。コードブロック内で実行されたテストは実際には結果としては出力されず、イベントとして扱えます。取り出したイベントを比較して、テスト関数が正しく動いていることをテストできます。イベントの比較には、eventビルダを使いチェックします。

次のコードでは、失敗するテストを例に、イベントを取り出して出力結果が意図したとおりかどうかをテストしています。

use Test2::Util::Table qw/table/;
is intercept {
  is [1, 2], [0];  ――(1)
}, array {
  event Ok => { pass => 0 };  ――(2)
  event Diag => {
    message => match qr{^\n?Failed test},
  };
  event Diag => {
    message => join "\n", table(          
      header => [qw/PATH GOT OP CHECK/],  
      rows => [                           
        ['[0]', '1', 'eq', '0'],          
        ['[1]', '2', '!exists',           
        '<DOES NOT EXIST>'],              |(3)
      ],                                  
    ),                                    
  };                                      
  end;                                    
};                                        

(1)では、イベントを取り出す対象となるテスト関数を呼び出しています。

(2)では、eventビルダにより、Okイベントが発行されていてその結果が失敗していることをチェックしています。

(3)では、出力結果のテーブルを比較しています。message自体は文字列であるため、Test2::Util::Tableにあるテーブルを作るヘルパを利用しています。

なお、Test2::Tools::EventDumperを使うことで、イベントからテストコードを生成できます。テストを書き始める際に使うと全体的なイベントの流れが理解しやすくなるでしょう。

テストモジュールの一例

ここでテストモジュールの一例として、筆者が開発したTest2::Tools::JSONというモジュールを紹介します。このモジュールは、JSONの文字列をテスト内で比較できるようにしたもので、Test::Deep::JSONをTest2用に書きなおしたものです。

使い方は、次のようにTest2::Tools::JSONがエクスポートするjson関数を使うことで、データ構造に含まれるJSONの文字列に対して比較を行えます。

use Test2::Tools::JSON;
is {
  foo => 'bar',
  payload => '{"a":1}',
}, {
  foo => 'bar',
  payload => json({a => E}),
};

上記のテスト中のJSONを{"a":2}と書き換えて失敗させてみます。図2のように、出力結果をJSON向けにカスタマイズできるところがTest2の便利なところです。

図2 失敗したテストの出力
# +-----------------------+---------+------+-------------+-----+
# | PATH                  | GOT     | OP   | CHECK       | LNs |
# +-----------------------+---------+------+-------------+-----+
# | {payload}             | {"a":1} | JSON | HASH(省略)| 4   |
# | {payload} <JSON>->{a} | 1       | eq   | 2           |     |
# +-----------------------+---------+------+-------------+-----+

まとめ

本稿では、新たなテスティングフレームワークであるTest2を使ったテストの方法について解説しました。Test2では、Test::Builderの問題を解決し、Test2::V0といった高機能なテストモジュールが使え、さらに従来のモジュールと比べるとテスト関数のテストが柔軟に行えます。

本稿で紹介したものは一部にすぎません。RSpecライクなテスト記法を提供するTest2::Workflowや、proveを置き換える形のテストランナーであるyathなど、Test2に関連する新たなツールが開発されています。みなさんも、これからテストを書く際はTest2を使ってみてください。

さて、次回の執筆者は小林謙太さんで、テーマは「Perlで堅牢な開発」です。お楽しみに。

おすすめ記事

記事・ニュース一覧