AUTOLOADによる他クラスへのメソッドの委譲のテスト

最近,Net::Qiita http://search.cpan.org/~yuuki/Net-Qiita-0.03/lib/Net/Qiita.pm というモジュールを作った. そのモジュールの中で,Net::QiitaクラスのAUTOLOADで,Net::Qiita::Clientが持っているメソッドを動的に呼び出す(委譲するとかいうらしい)ということをやった. Rubyでmethod_missingでメソッド名を取得してrespond_toしてsendする感じ.

use Net::Qiita::Client;

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

    Net::Qiita::Client->new(\%options);
}

# Delegate method to Net::Qiita::Client object
sub AUTOLOAD {
    my $func = our $AUTOLOAD;
       $func =~ s/.*://g;
    my (@args) = @_;

    {
        no strict 'refs';

        *{$AUTOLOAD} = sub {
            my $class  = shift;
            my $client = $class->new;
            defined $client->can($func) || croak "no such func $func";
            shift @args;
            $client->$func(@args);
        };
    }
    goto &$AUTOLOAD;
}

そもそも,わざわざメタなことをする必要があったのかということはまた別に書くとして,メソッドの委譲処理が仕様どおりかどうかテストする方法を考えていた.

まず考えたこと

Test::Moreの can を使ってクラスがメソッドをもっているかどうかを確認できると考えた.

ok Net::Qiita->can('user_items'); #Net::Qiitaクラスがuser_itemsメソッドをもっているかどうか

# can_okを使うほうがスマートだけど,説明の都合上直接canを直接使う

これは結論から言うとダメで,理由は perldoc に書いてあった.

can はオブジェクトが AUTOLOAD を通してメソッドを提供可能かどうかは 知ることができません, そのため undef が返ってきてもオブジェクトが そのメソッド呼び出しを処理することができないとは限りません. これを 回避するにはモジュールの作者が AUTOLOAD を使って処理するメソッドに対して 前方宣言を使うことです(perlsub参照). そのような'ダミー'の関数は can はコードリファレンスを返しますが, それが呼び出された時には AUTOLOAD へとフォールスルーされます.

UNIVERSAL::canは現在のクラスと親クラスのメソッドしか探さないみたい. (UNIVERSALクラスの"system"を引数にとるとundefになるので,UNIVERSALクラスは探さないっぽい.)

解決策は,perldocに書いてある通り,use subsを使って前方宣言すればよいらしい. ただし,use subsを使うやり方のデメリットとして以下の2つを考えた, - 委譲を許すメソッド名を列挙しなければいけなくてDRYに反する. - AUTOLOAD内の処理をテストできない(前方宣言さえしていればcanはコードリファレンスを返してしまってAUTOLOADを実行しない)

メリットとしてはhitodeさんに言われたけれど補完が効くことが挙げられる.

採用したやり方

AUTOLOAD内の処理をテストできないのが一番よくないと思っていて,(何かやり方ないかな) 結局テストは以下のように書いた.

use Test::More;
use Test::Fatal;
use Test::Mock::Guard;

my $stub_ref = sub { return 1 };

my $user_mock_funcs = +{
    user_items           => $stub_ref,
    user_following_tags  => $stub_ref,
    user_following_users => $stub_ref,
    user_stocks          => $stub_ref,
    user                 => $stub_ref,
};

my $mock = mock_guard 'Net::Qiita::Client::Users', $user_mock_funcs;

for (keys %$user_mock_funcs) {
    is Net::Qiita->$_, 1;
}

like exception {Net::Qiita->nainai; }, qr(no such func);

委譲するメソッドを例外を投げるだけのスタブにして,その例外をキャッチできたら,正しく委譲できているとしている. hitodeさん「例外投げなくても1とか返せばいいんじゃないですか」「はい」

例外投げる意味何もなかったので,1返すようにした.

デメリットは,Test::Mock::GuardとかTest::MockObjectでスタブ化しないといけないので,テストコードが冗長になってしまうこと. ただ,本体のコードが冗長になるよりは,テストコードが冗長になる方がマシだと思っているのでとりあえずこうしている.

サボるとしたら,スタブ化をやめてno such func以外の例外メッセージをキャッチしたら正しいみたいにするか.

unlike exception {Net::Qiita->user_items}, qr(no such func)

ただし,今回の場合はuser_itemsの中でHTTPリクエストを投げるので,タイムアウトするまでテストが終わらないとかになってダサいので,スタブにしてる.

ベストプラクティスほしい.

最近使ったPerlのテスト系モジュール

最近Perlを書いていて,便利なテスト系モジュールをいくつか使ったのでメモ.

Test::Moreのsubtestメソッド

RSpecのitメソッドみたいな感じ.
テストをブロックに分けてそれぞれのブロックに名前を付けることができるイメージ.

use Test::More;

subtest "description" => sub {
    # テスト書く
}; 

Test::Mock::LWP::Conditional

LWP::UserAgentのリクエストスタブ.
LWP::UserAgentで任意のURLに対応するレスポンスを指定できる.

    my $res = HTTP::Response->new(200);
    $res->content("インターネット");
    Test::Mock::LWP::Conditional->stub_request("www.internet.com" => $res);
    
    # 以降,LWP::UserAgentで"www.internet.com"をGETしたらステータスコード200でcontentが"インターネット"で返ってくる

Test::MockObject::Extends

既存のクラスに含まれるメソッドをMockに置き換える.(Test::Mockは既存のクラスをMockに置き換える)

    # インスタンスメソッドをMockに置き換え
    my $internet = Net::Internet->new;
    my $mock = Test::MockObject::Extends->new($internet);
    $mock->set_always("service", +{ hatena => "www.hatena.ne.jp" });

    is $mock->service->{hatena}, "www.hatena.ne.jp";

    # newの引数にクラス名を指定することで,クラスメソッドを置き換えることもできる
    my $mock = Test::MockObject::Extends->new("Net::Internet");
    $mock->set_always("static_service", +{ twitter => "twitter.com" });

    is $mock->static_service->{hatena}, "twitter.com";

Test::Exception

croakやdieで投げた例外をテストできるモジュール.

    # doメソッドがcroak "No Such Internetを含むエラーメッセージ"を投げるかどうかテスト
    throws_ok {
        my $internet = Net::Internet->do("Facebook");
    } qr(No Such Internet);

Test::Mock::Guard

Mockオブジェクトを作成するモジュール.
Test::MockObjectよりもシンプル.

    # Net::InternetクラスをMock化する
    my $mock = mock_guard 'Net::Internet',
        +{
            do => sub { 
                my $self = shift;
                return +{ SAO => 'SwordArtOnline' };
             },
        };

    my $internet = Net::Internet->new;
    is $internet->do->{SAO}, 'SwordArtOnline';

Kyoto.pm Tech Talk #02に参加してきた

id:shibayu36 さん主催のKyoto.pm Tech Talk #02に参加してきた.
インターン終了から1年ぶりにはてなを訪れたことになる.
久々にはてなの社員の方々や今年度のインターン参加者の方々とお話ができてテンション上がった.
(実はインターン以来Perl触ってない)

id:motemenさんが紹介されていたDBIx::Liteはメソッドチェーンできて便利そう.
あとは,Text::Hatenaってずっと放置されてると思ってたけど,実ははてな記法パーサは何種類かあって最新のものをid:onishiさんが紹介してた.

自分も何か発表したいと思ったけど,Perlは全然書いてなかったので,代わりに先月作ったDailyCodingについてLTした.
スライドはこっちKyoto.pm #02 DailyCoding - プログラマのための暇つぶしサービス
Web系の人たちよりも競技系の人たちのほうがウケは良い気がする.

懇親会では,インターン生が持ってるAndroid端末をおもちゃ呼ばわりする簡単なお仕事をしたり,ルビーに土下座するルビーストが居たり,変わり果てた某社員様が居たり,id:pokutunaさんと一緒に現インターン生に先輩風吹かせたり,なかなかインターネットっぽくて楽しかった.
f:id:y_uuki:20120823035250p:plain
f:id:y_uuki:20120823035303p:plain
※ルビーストです
f:id:y_uuki:20120823035255p:plain
※社員様です