Masteries

技術的なことや仕事に関することを書いていきます.

小ネタ: Perlのメソッド呼び出しをModule::Spy風にモックする

Perlでテストを書いているとき, 例えば外部のAPIを叩くメソッドをモックしたい, という気持ちになることがあります. 選択肢としてはTest::Mock::GuardやModule::Spy, 最近ならTest2::Tools::Mockあたりが選択肢になるでしょうか.

metacpan.org

metacpan.org

metacpan.org

「単純にモックしたい」という用途なら, 上記に挙げた3つのモジュール全てがその望みを叶えてくます. ただ, 「モックしたメソッドに渡った引数もチェックしたい」のであれば, Module::Spyが最適でしょう.

use DDP;
use Module::Spy;

my $spy = spy_on('Hogehoge', 'hoge_method')->and_returns(undef);

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

p $spy->calls_most_recent;
# \ [
#     [0] "Hogehoge",
#     [1] "args1",
#     [2] "a",
#     [3] "args2",
#     [4] "b"
# ]

package Hogehoge;

sub hoge_method { ... }

一方で, この時に hoge_method に渡った args1 をテストしたい... という場合は, 次のように書く必要があります.

use DDP;
use Module::Spy;

my $spy = spy_on('Hogehoge', 'hoge_method')->and_returns(undef);

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

my (undef, %args) = $spy->calls_most_recent->@*;
is $args{args1}, 'a';

Module::Spyは配列リファレンスを返し, かつその最初にはオブジェクト or パッケージ名の文字列が格納されているため, その手当も必要です. 数度ならいいですが, 都度都度こういった手当をするのは少々手間です. というわけで次のような, Module::Spy風のモジュールを使って処理するのはどうでしょうか.

package Test::Mock::Hogehoge;
use strict;
use warnings;
use utf8;

use Exporter 'import';

use Test2::Tools::Mock qw(mock);

our @EXPORT_OK = qw(mock_hogehoge);

sub mock_hogehoge {
    my $guard = Test::Mock::Hogehoge->new();
    $guard->{guard} = mock 'Hogehoge' => (
        override => [
            hoge_method => sub {
                my (undef, %args) = @_;
                $guard->_push_args(\%args);
            },
        ]
    );

    return $guard;
}

sub new {
    my ($class) = @_;

    bless { args => [] }, $class;
}

sub _push_args {
    my ($self, $args) = @_;

    push $self->{args}->@*, $args;
}

sub calls_all {
    my ($self) = @_;
    return $self->{args};
}

sub called {
    my ($self) = @_;
    return scalar $self->calls_all->@* == 0 ? !!0 : !!1;
}

sub calls_most_recent {
    my ($self) = @_;
    return $self->called ? $self->calls_all->[-1] : undef;
}

sub calls_first {
    my ($self) = @_;
    return $self->called ? $self->calls_all->[0] : undef;
}

sub calls_reset {
    my ($self) = @_;
    $self->{args} = [];
}

1;

これなら, 次のように書けて比較的ラクではないでしょうか. インターフェイスや使い方もModule::Spyに準拠しているので, 覚えることを増やさずにテストを簡単に書けて嬉しくなりそうです.

use Test::Mock::Hogehoge qw(mock_hogehoge);
use Test::More;

my $mock = mock_hogehoge;

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

is $mock->calls_most_recent->{args1}, 'bb';

テストを書くのは大事, とわかりつつ, ダラダラ書くのは大変という気持ちもあると思っていて, 言語を問わずこういう感じで少しでも短く, 簡単に書けるような取り組みをしていくのが大事かな, と思っています. 小粒すぎる技ではありますが, 今後もおもしろいアイデアが浮かんだらブログに書いていこうと思いました.