Perl Hackers Hub

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

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

実例を交えた使い方

Test2::V0には、比較以外にもテストを書くときによく使うツールセットが用意されています。ここで代表的なものを紹介します。

オブジェクトのモック

外部APIに接続する処理をテストする際に、クラスやオブジェクトのメソッドを書き換えて特定のレスポンスを返すようにモックすることがあります。直接メソッドを書き換える方法もありますが、それではテスト後に書き換えたメソッドを戻すことやネストしたときの扱いに気を付ける必要があります。

mock関数を使うと、宣言されたスコープ内でのメソッドの上書きできます。

package Foo {
  sub doit { 'A' }
}
{
  my $mock = mock 'Foo' => (
    override => [doit => sub { 'B' }],  ――(1)
  );
  is +Foo->doit, 'B', '書き換えられた';
}
is +Foo->doit, 'A',
  'スコープを抜けるともとのメソッドが呼ばれている';

(1)では上書きする対象となるメソッド名とそのコードリファレンスをパラメータoverrideに渡しています。クラスメソッドを呼び出すと上書きした値が返ってきており、スコープから抜けるとモックが無効になっていることがわかります。

特徴的なのは、メソッドに対して事前にある動作を行うbefore事後に行うafterそして前後に行うaroundのフックが登録できるようになっていることです。次のコードのようにaroundを使うことで、あるメソッドに渡された引数やその戻り値を横取りできます。

my $mock = mock 'Foo' => (
  before => [doit => sub { ... }],
  after => [doit => sub { ... }],
  around => [doit => sub {
    # もとのメソッドのコードリファレンス
    my $orig = shift;
    # メソッドの引数
    my (@args) = @_;
    # もとのメソッドを実行した結果
    return $orig->(@args);
  }],
);
Foo->doit('foo');

例外の捕捉

異常系のテストを行う際に、コード内で発生した例外を捕捉したいことがあります。

例外が発生した際のメッセージの比較、さらには例外が発生しないことのテストを書いてみます。

sub foo { die 'no params' unless @_ }
like dies { foo() }, qr/no params/, '例外が発生した';
ok lives { foo(1) }, '例外が発生しない';

1行目では、引数を渡さなかった場合に例外を発生させる関数を宣言しています。

2行目では、dies関数に渡したブロック内で発生した例外を戻り値として、文字列との一致をテストしています。続く3行目では、lives関数を使って例外が発生しないことをテストしています。

警告の捕捉

Perlのwarnによる警告を捕捉し、警告の有無のチェックや内容の比較をしてみます。

警告が出力される関数について、テストを書いてみます。

sub foo {
  warn 'foo';
}
ok warns { foo() }, '警告された';  ――(1)
like warning { foo() }, qr/foo/,  
  '警告された内容のチェック';     ┛(2)

(1)では、自分で作成したfoo関数内で警告が発生したかどうかをテストしています。さらに発生した警告の内容のチェックするため、(2)ではwarning関数が返す値を見ています。

なお、CPAN上に公開されているTest2::Plugin::NoWarningsを使って、テスト実行中に警告が発生した際にテスト自体を失敗させることができます。

ほかにもTest2::Tools::といったネームスペース以下にTest2用のツールセットがいくつか公開されていますので、Test2::V0が提供するものだけでは足りないと思ったときはCPAN内を検索してください。

日本語を出力する場合

テストに与える説明文として日本語を書いたときは、文字列の内部表現をそのまま出力しようとするため、Perl側でWide character in printの警告が出ることがあります。

よくある回避方法として、Test::Builderで出力する際にファイルハンドルに対してエンコーディングを指定する方法があります。Test2::V0ではこのような回避は不要となり、Test2::Plugin::UTF8をあらかじめインポートしておくことで解決してくれます。

my $builder = Test::More->builder;
binmode $builder->output, ":encoding(utf8)";
binmode $builder->failure_output, ":encoding(utf8)";
binmode $builder->todo_output, ":encoding(utf8)";

ただしTest2::V0をインポートしていると、デフォルトでTest2::Plugin::UTF8が有効になるため特に気にする必要はないのですが、既存のテストで上記のコードが書かれている場合には注意してください。

乱数のシードを設定する

Test2::V0内で有効になるプラグインTest2::Plugin::SRandでは、乱数を初期化するためにsrandを呼び出すようになっています。テスト実行時には次のようにメッセージが出力され、特定のシードによる乱数が初期化されていることがわかります。このシードとは、乱数の初期状態を決める際に用いられる値です。シードが同じであれば、あらためて乱数を生成しても同じ状態を作ることができます。

# Seeded srand with seed '20180625' from local date.

デフォルトでは乱数のシードは現在日時の形式になっているため、テスト用のデータ生成に乱数を使っている場合には同じ乱数が使われないように気を付ける必要があります。

そこで、Test::V0がインポートされる前にTime::HiResgettimeofdayを使ってマイクロ秒単位のエポック秒(UNIX時刻)をシードとすることで、現実的に被らないようにすることがお勧めです。環境変数T2_RAND_SEEDに値をセットしておくことでシードとして設定されるため、同じ乱数でテストを再現したい場合にも使えます。

BEGIN {
  use Time::HiRes qw(gettimeofday);
  $ENV{T2_RAND_SEED} //= join '', gettimeofday;
}

Test::Moreとの非互換なコード

ここでは、Test::Moreを使った既存のテストをTest2::V0に移行してみましょう。

多くの場合、use Test::More;している行をuseTest2::V0;に置き換えることで移行できるはずですが、一部の非互換なコードに気を付ける必要があります。

データ構造の比較―is_deeply

Test::Moreではデータ構造の比較にはisではなくis_deeplyが使われていましたが、Test2::V0で提供されるのはisのみです。ただし、振る舞い自体は変わらないため、単純な置換だけで書き換えができます。

Test::More
is_deeply $x, [1, 2, 3];
Test2::V0
is $x, [1, 2, 3];

オブジェクトのクラスのチェック─⁠─isa_ok

Test2::V0では、多重継承されているオブジェクトに対して一度で複数のクラスを渡してチェックできるようになりました。テストの説明文を渡す際には、第2引数Perl Hackers Hubに配列のリファレンスを経由する必要があります。

Test::More
isa_ok $x, 'Foo', 'テストの説明文';
Test2::V0
isa_ok $x, ['Foo'], 'テストの説明文';
isa_ok $x, 'Foo'; # 説明文が不要の場合

モジュールのロードのテスト

モジュールの文法エラーやロードした際のランタイムエラーがないことを確認するために、Test::Moreではrequire_okuse_okといったテスト関数が使われていました。Test2::V0ではこれらの関数が廃止されたため、require_okの代わりにrequireを直接呼び出すように変更する必要があります。モジュールのロードが失敗したときの例外によりテストは失敗するため、挙動は以前と変わりません。use_okも同様の理由でuseに変更する必要があります。

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

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧