日向夏特殊応援部隊

俺様向けメモ

Test::Mock::LWP を試す

久しぶりのエントリです。一応元気にやってますよっと。

ここまでのあらすじ

同僚の id:typomaster さんから、Test::Mock::LWP を使ってテスト書きたいんだけど〜と言われたので早速軽く試してみましたよ。

Test::Mock::LWP とは

具体的には、

  • LWP::UserAgent
  • HTTP::Request
  • HTTP::Response

モジュールに対して Test::MockObject 化した物だと考えて良い。それぞれ、

  • Test::Mock::LWP::UserAgent
  • Test::Mock::HTTP::Request
  • Test::Mock::HTTP::Response

モジュールが対応しています。それぞれソースは短いので実際に見てみましょう。

Test::Mock::HTTP::Request
package Test::Mock::HTTP::Request;
use strict;
use warnings;
use Test::MockObject;
use base 'Exporter';
our @EXPORT = qw($Mock_req $Mock_request);

our $Mock_req;
our $Mock_request;

our $VERSION = '0.01';

BEGIN {
    $Mock_request = $Mock_req = Test::MockObject->new;
    $Mock_req->fake_module('HTTP::Request', 
        new => sub { $Mock_req->{new_args} = [@_]; $Mock_req });                       
}                                                                          
$Mock_req->set_always('authorization_basic', '');
$Mock_req->set_always('header', '');
$Mock_req->set_always('content', '');

sub new { $Mock_req };
$Mock_req->mock('-new_args', sub { delete $Mock_req->{new_args} });

package HTTP::Request;

our $VERSION = 'Mocked';

まぁすぐ分かるけど、$Mock_req, $Mock_request ってのは同じ値として Test::MockObject のインスタンスが代入されていて、HTTP::Request を fake しますよと。new した時は new_args フィールドに配列リファレンスとして引数を格納します。

set_always ってのは指定したメソッドの戻り値のデフォをどうするかって話。

Test::Mock::HTTP::Response

こいつも Request と大体似たような感じ

package Test::Mock::HTTP::Response;
use strict;
use warnings;
use Test::MockObject;
use base 'Exporter';
our @EXPORT = qw($Mock_resp $Mock_response);

our $Mock_resp;
our $Mock_response;

our $VERSION = '0.01';

BEGIN {
    $Mock_response = $Mock_resp = Test::MockObject->new;
    $Mock_resp->fake_module('HTTP::Response');                       
    $Mock_resp->fake_new('HTTP::Response');
}                                                                          

our %Headers;
$Mock_resp->mock('header', sub { return $Headers{$_[1]} });
$Mock_resp->set_always('code', 200);
$Mock_resp->set_always('content', '');
$Mock_resp->set_always('is_success', 1);

package HTTP::Response;

our $VERSION = 'Mocked';

さっきと大体同じだったんだけど、今回は new しても引数を格納したりはしない。our %Headers を上手く使えばレスポンスヘッダとかのアクセサを簡単に定義できて、後は初期値は 200 OK なんですね。で、is_success は 1 を返すと。

Test::Mock::LWP::UserAgent
package Test::Mock::LWP::UserAgent;
use strict;
use warnings;
use Test::MockObject;
use Test::Mock::HTTP::Response;
use Test::Mock::HTTP::Request;
use base 'Exporter';
our @EXPORT = qw($Mock_ua);

our $Mock_ua;

our $VERSION = '0.01';

BEGIN {
    $Mock_ua = Test::MockObject->new;
    $Mock_ua->fake_module('LWP::UserAgent');                       
    $Mock_ua->fake_new('LWP::UserAgent');
}                                                                          

$Mock_ua->set_always('simple_request', HTTP::Response->new);
$Mock_ua->set_always('request', HTTP::Response->new);

package LWP::UserAgent;
use strict;
use warnings;

our $VERSION = 'Mocked';

見る限り、request, simple_request しか定義してないので、他のメソッド等使いたい場合は自分で何とかします。

Sample

なんかありがちな処理をテストにしてみよう。

use Test::More;
use Test::Mock::LWP;
use Test::Mock::HTTP::Response;

use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response;

plan tests => 6;

$Mock_request->set_isa('HTTP::Request');
$Mock_response->set_isa('HTTP::Response');
$Mock_ua->set_isa('LWP::UserAgent');

$Mock_response->mock( content => sub {
    return << 'CONTENT';
He should write blog entry as soon as posibble.
CONTENT
} );

%Test::Mock::HTTP::Response::Headers = (
    'Content-Type' => 'text/plain',
);

my $req = HTTP::Request->new(GET => 'http://example.com/');

isa_ok($req, 'HTTP::Request');

my $ua  = LWP::UserAgent->new;

isa_ok($ua, 'LWP::UserAgent');

my $res = $ua->request($req);

isa_ok($res, 'HTTP::Response');

ok($res->is_success, 'is request() success');
is($res->code, 200, 'status code');
is($res->header('Content-Type'), 'text/plain', 'match Content-Type header');

note($res->content);

試しに実行してみると、

$ prove -vc --timer typomaster.t
[00:48:32] typomaster.t ..
1..5
ok 1 - The object isa HTTP::Request
ok 2 - The object isa LWP::UserAgent
ok 3 - The object isa HTTP::Response
ok 4 - is request() success?
ok 5 - status code
# He should write blog entry as soon as posibble.
ok      228 ms
[00:48:33]
All tests successful.
Files=1, Tests=5,  1 wallclock secs ( 0.05 usr  0.09 sys +  0.17 cusr  0.06 csys =  0.37 CPU)
Result: PASS

みたいになる。

ちなみに LWP 使ってる既存のコードにも Test::Mock::LWP を使ったテストを組み込む事が出来ます。また挙動を色々再現する上で Test::MockObject の使い方は何となく知ってた方が良いと思います。

とりあえず手軽に使えるよと。

まとめ

  • 経緯はともかく、叩いてる箇所だけ埋めればいいので基本的にお手軽
  • 但し裏を返せば多少複雑な事をやってる際にはそれが仇になるかも
    • Test::HTTP::Server::Simple とかと使い分けるといいかも

とりあえず、ブログちゃんと書いてください > id:typomaster