そう言えばこないだのうどん屋のコードは一切テストを書かなかったけど、それはよろしく無い、まったくもって主義に反するし、RubyのときはちゃんとSpec書いたのにPerlのときは書かないだとかふざけてる、と思ったのでテストも書いてみることにした。

さてテストだけど、HTTP::Engineにはちゃんとテスト用のインターフェースが用意されている。あと、テストリクエストを生成するモジュールもある。なんだ、じゃあ話は簡単だ。

  1. interface => { module => 'Test' } でengineを作る
  2. HTTP::Engine::Test::Requestでrequestを作る
  3. engineのrunメソッドにテストリクエストを投げてやる
  4. 返ってきたレスポンスをチェックする

ってことですね、わかります。

まずは素直に書いてみる

コード量少ないのではっつけちゃおう。Udon::AppにGETリクエストを投げるテストはこんな感じ。

use strict;
use warnings;
use FindBin qw($Bin);
use Udon::App;
use Test::More;
use HTTP::Engine::Test::Request;

plan tests => 1;

my $app = Udon::App->new( { viewdir => "$Bin/../view" } );
my $engine = $app->setup_engine( { module => 'Test' } );
my $req = HTTP::Engine::Test::Request->new(
    uri => 'http://udon.example.org/',
    method => 'GET'
);
my $res = $engine->run($req);
is $res->code, 403, 'should return "Forbidden" when GET request';

ふむ、まぁ、簡単ですね!あとはこんな感じでどんどん$reqを作ってどんどん$engine->runしてやれば良い。

Test::Declare

ところで、Test::Moreはまぁ見慣れてるんで使い方に迷うことは無いんだけども、普段からRSpecが大好きで勢いあまってObjective-CのテストにまでRSpecを使っちゃう僕としては少々見栄えが気に入らない。のでTest::Declareってやつを使ってみることにした。

use strict;
use warnings;
use FindBin qw($Bin);
use Udon::App;
use Test::Declare;
use HTTP::Engine::Test::Request;

plan tests => blocks;

describe 'GET' => run {
    my $res; 
    init {
        my $app = Udon::App->new( { viewdir => "$Bin/../view" } );
        my $engine = $app->setup_engine( { module => 'Test' } );
    };
    test 'should return "Forbidden"' => run {
        $res = $engine->run(
            HTTP::Engine::Test::Request->new(
                uri => 'http://udon.example.org/',
                method => 'GET'
            ),
        );  
        is $res->code, 403;
    };  
};  

ようし、少し見栄えが良くなった。いや、べ、別に describe が入ってるから気に入ったんじゃないんだから。えーとほら、こうして何に対するテストなのかの説明と順番とテストコードとがひとまとめになってた方がわかりやすいじゃん。ね?

それは良いんだけど、setup_engineとかHTTP::Engine::Test::Request->newとかが今度は美しくない。このコードだと一個しか書いてないからまだあれだけど、「彼女が404」のSpecくらいに網羅しようと思うとちょっとげんなりする。そんなもん大体同じなんだから何度も書きたくないし、見にくい。

Test::HTTP::Engine

そう言えばRackのときはRack::Testを使ったら劇的にさっぱりした。じゃあ同じ方法で解決してみれば良いんじゃなかろうか。と思い立って適当にこんなものを拵えてみた。

package Test::HTTP::Engine;

use strict; 
use warnings;
use Exporter;
use HTTP::Engine::Test::Request;
our @ISA    = qw(Exporter);
our @EXPORT = qw(engine get);

sub engine { die }
    
sub get {
    my $path = shift;
    engine->run(
        HTTP::Engine::Test::Request->new(
            uri    => "http://example.org/$path",
            method => 'GET'
        ),  
        env => \%ENV
    );
}   

1;

で、これを使うとさっきのテストはこうなる。

use strict;
use warnings;
use FindBin qw($Bin);
use Udon::App;
use Test::Declare;
use Test::HTTP::Engine;

plan tests => blocks;

# engineの生成は各テストで上書きして変える
no warnings 'redefine';
*Test::HTTP::Engine::engine = sub {
    my $app = Udon::App->new( { viewdir => "$Bin/../view" } );
    $app->setup_engine( { module => 'Test' } );
};
    
describe 'GET' => run {
    test 'should return "Forbidden"' => run {
        is get('/')->code,    403;
        is get('/get')->code, 403;
    };  
    test 'should return "I\'m a teapot" with mode="prev"' => run {
        is get('/?mode=prev')->code, 418;
    };
    test 'should return "Gone" with mode="next"' => run {
        is get('/?mode=next')->code, 410;
    };
};

さっぱりした。しれっとテスト増やしたけどさっきより見易いし、そこはかとなくRSpec版に近付いた気がするぞ。あとはpostとかdeleteとかも作ってやればRack::Testでやったのと近いことができる。思い付きで作ったけど意外と良いな。

テストも簡単だから怠けてないで書けよと

というわけでApacheやServerSimpleでサーバプロセスを立ち上げたりしなくてもサクっとアプリのテストが出来ちゃって良いですね。こんな簡単なら最初からテスト書けよって言われそうですね。や、やりますよ。ちゃんと後でうどん屋のやつにもテスト追加しときますって。ひぃ。

追記

自分でもTest::HTTP::Engine::engineを毎回置き換えるのは無いよなー、なんか違うなーと思ってたら、yappoさんから

engine {}; で setup したほうがいかもー

とのこと。という訳で手直し中。