メメメモモ

プログラミング、筋トレ、ゲーム、etc

Test::ExceptionとTest::MockObjectを使ってテスト

Test::ExceptionとTest::MockObjectを使用したテストを書いてみました。
それぞれのモジュールは下記のような機能を持っています。

Test::Exception

例外のテストを行なうモジュールです。
下記のようなメソッドが定義されています。

lives_ok     { test_code() } "test_code()は例外を投げずに正常終了する";
dies_ok      { test_code() } "test_code()は例外を投げ終了した";
throws_ok { test_code() } "ExceptionObject", "test_code()はExceptionObject例外を投げて終了した";

Test::MockObject

オブジェクトを偽装するモジュールです。
下記のようにTest::MockObjectのインスタンスを生成し、偽装メソッドを定義していきます。

my $mock = Test::MockObject->new;
$mock->mock( 'foo' => sub {
   ok(1, 'foo() got called');
});

テスト

テストしたモジュールは、ItemManagerというものです。

package ItemManager;
use strict;
use warnings;

sub new {
    my $class = shift;
    bless {@_}, $class;
}

sub put_item {
    my $self = shift;
    my ($item_number, $count) = @_;

    # 1
    $self->{item_hash}->{$item_number} ||= 0;
    my $rest = $self->{item_hash}->{$item_number};
    if ( $rest < $count ) {
        # 2
        die bless {}, "ItemShortageException";
    }

    # 3
    for (my $i = 0; $i < $count; $i++) {
        eval {
            # 4
            $self->{item_unit}->put_item( $item_number );
        };
        if ($@) {
            #5
            die bless {}, "ItemUnitJammedException";
        }
    }
    # 6
}

sub set_item_unit {
    my $self = shift;
    my ($item_unit) = @_;
    $self->{item_unit} = $item_unit;
}

1;


主にput_item()メソッドをテストしています。


put_item()メソッドでは、条件が合わない場合、例外を投げます。
この例外はTest::Exceptionでテストします。


また、ItemManagerは、ItemUnitオブジェクトを保有しています。
ItemUnitはまだ実装していないことを想定しているので、
Test::MockObjectで偽装します。


テストケースでは下記の処理経路を辿るようにしました。
番号はコメントで記述されているものです。

  • 1 - 2 (例外終了)
  • 1 - 2 - 3 - 4 - 5 (例外終了)
  • 1 - 2 - 3 - 4 - 3 - 6 (正常終了)
use strict;
use warnings;

My::Test->runtests;


package My::Test;
use base qw/Test::Class/;
use Test::More;
use Test::MockObject;
use Test::Exception;
use ItemManager;


sub make_fixture : Test(setup) {
    my $self = shift;
    $self->{item_manager} = ItemManager->new( item_hash => { 10 => 10 } );
}

sub test_instance : Test(no_plan) {
    my $im = shift->{item_manager};

    isa_ok $im, 'ItemManager';
}


sub test_put_item : Test(no_plan) {
    my $self = shift;
    my $im = $self->{item_manager};

    # Itemが足りないとき、ちゃんと例外を投げるかどうか( 1 -2 )
    throws_ok {
         $im->put_item( 10, 11 );
    } 'ItemShortageException', '$im->put_item()は,ItemShortageExceptionを投げて終了した。';


    # forループが正常に終わるか ( 1 - 2 - 3 - 4 - 5 ) 
    my $mock = Test::MockObject->new;
    $mock->mock('put_item' => sub {
       ok(1, 'put_item() got called');
    });
    $im->set_item_unit($mock);

    $im->put_item( 10, 1 );


    # item_unitが例外を投げた時、ItemManagerが正常に例外を投げるか ( 1 - 2 - 3 - 4 - 3 - 6  )
    $mock->mock('put_item' => sub {
       die "UnitJammedException";
    });

    throws_ok {
       $im->put_item( 10, 1 );
    } "ItemUnitJammedException", '$im->put_item()は,ItemUnitJammedExceptionを投げて終了した。';
}


正常にテストが完了したら下記のように結果が表示されます。

$ prove -lv t/00.t    

Name "main::running_under_some_shell" used only once: possible typo at /usr/bin/prove5.10.0 line 3.
t/00.t .. # 
# My::Test->test_instance

ok 1 - The object isa ItemManager
# 
# My::Test->test_put_item
ok 2 - $im->put_item()は,ItemShortageExceptionを投げて終了した。
ok 3 - put_item() got called
ok 4 - $im->put_item()は,ItemUnitJammedExceptionを投げて終了した。
1..4
ok
All tests successful.
Files=1, Tests=4,  0 wallclock secs ( 0.05 usr  0.02 sys +  0.18 cusr  0.04 csys =  0.29 CPU)
Result: PASS

参考

モダンPerl入門 (CodeZine BOOKS)

モダンPerl入門 (CodeZine BOOKS)

WEB+DB PRESS Vol.46

WEB+DB PRESS Vol.46