As Sloth As Possible

可能な限りナマケモノでありたい

タグ:HTTP::Engine

そう言えばこないだのうどん屋のコードは一切テストを書かなかったけど、それはよろしく無い、まったくもって主義に反するし、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 したほうがいかもー

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

先週末からこっちうどん屋をいじって遊んでたのだけど、HTTP::EngineにはHTTP::Engine::Middlewareというミドルウェアを作る仕組みもあるので、せっかくなのでUdonMap(Rack::URLMapもどき)をミドルウェアで実装してみた。

gist: 117012 - GitHub

前回の記事のときと構成が変わってるけど気にしない方向で。あとで前の記事直しとこう前の記事も補足した。Udon::Middleware::URLMapがミドルウェアで、server-middleware.plがミドルウェア版UdonMapを使って書き直したもの。

ミドルウェア自体は簡単に作れた。use HTTP::Engine::MiddlewareするとMooseの初期化処理をしたりミドルウェア用のメソッドを生やしたりしてくれるので、HTTP::Engineをnewするときに指定したハンドラより前に実行する処理を作りたいときは、before_handleでHTTP::Engine::Requestを受けとってHTTP::Engine::Responseを返す処理を書いてやればいいだけ。 そのミドルウェアが処理すべきRequestじゃないときや、Requestを加工して次に渡したいときは、ResponseじゃなくてRequestを返してやると次のミドルウェアやハンドラに処理が渡る様子。簡単簡単。あとモジュールの最後に__MIDDLEWARE__を書くのを忘れずに。これで後処理もしてくれる。

ちょい補足。厳密に言うと、before_handleの返す値がHTTP::Engine::Responseだったときに限り、そこでbefore_handleのループが終了する。なので次のミドルウェアに処理を回したいときは、別にHTTP::Engine::Requestを返さなきゃいけないわけではない。ただ、前のミドルウェアが返した値がそのまま次のミドルウェアもしくはrequest_handlerにリクエストとして渡るので、普通はHTTP::Engine::Requestかそれと同じ振る舞いをするオブジェクトを返すものだと思う。そうしないとミドルウェアを重ねられないし。

で、その後request_handler、after_handleの処理へと続くんだけど、もしbefore_handleがあるのうちのどれかがHTTP::Engine::Responseを返してた場合はrequest_handlerは呼ばれない。でもafter_handleはエラーでも出てない限りは呼ばれるし途中で止まることもない。つまりafter_handleを持ってるミドルウェアは全部実行される。という挙動のはず。

ところで実はこれ自体はさくっと作れたんだけど、公開しようと思ってファイルの場所移してモジュールの名前変えたらいきなり動かなくなってちょっとだけハマった。最初に書いてたときはNetaKit::Middleware::URLMapって名前にしてたんだけど、UdonMap2って名前にすると何故か「before_handleだの__MIDDLEWARE__だの、そんなもんねーよ、アホか」とPerlさんに怒られる。なんじゃろ、と思いつつソース読んでるときにその理由に気付いた。HTTP::Engine::Middlewareをuseしてるやつのモジュール名の中にMiddlewareってのが含まれてないと、そのモジュールをミドルウェアとして初期化してくれない。

# HTTP/Engine/Middleware.pmから抜粋
sub import {
    my($class, ) = @_;
    my $caller = caller;

    return unless $caller =~ /(?:\:)?Middleware\:\:.+/;

    strict->import;
    warnings->import;

    init_class($caller);

    if (Any::Moose::moose_is_preferred()) {
        Moose->import({ into_level => 1 });
    } else {
        Mouse->export_to_level( 1 );
    }

    no strict 'refs';
    *{"$caller\::__MIDDLEWARE__"} = sub {
        use strict;
        my $caller = caller(0);
        __MIDDLEWARE__($caller);
    };

    *{"$caller\::before_handle"}     = sub (&) { goto \&before_handle     };
    *{"$caller\::after_handle"}      = sub (&) { goto \&after_handle      };
    *{"$caller\::middleware_method"} = sub     { goto \&middleware_method };
}

なんでかなーとちょっと考えたんだけど、ミドルウェア作るときも使うときもuse HTTP::Engine::Middlewareなんだけど、importの中でいろいろと前処理してるので、何もしなければ使う側のときでもMooseの初期化したりbefore_handleを生やしたりしてしまう。だから多分モジュール名に制約を付けておけばよしなにやってくれるようにしてるんだろう。とか思ってたらちゃんとそう言ってるじゃん。気付けよ俺…。

というわけでHTTP::EngineとHTTP::Engine::Middlewareの使い方がなんとなく分かってきたので、そろそろうどん屋と戯れるのは終わりにして、一通りWAFっぽい動きをするものでも作ってみようかな。

このブログではまず滅多にPerlの話を書かないのだけど、実は仕事ではPerlばっかり書いてたりする。にもかかわらず最近RubyRuby言いすぎなので、このままではマズい、社内のPerl Mongerな方々にトゥシューズに画鋲を入れられたり机に花を飾られたりしてしまう(※)、ということで今週末はHTTP::Engineをいじってみることにした。

※Perl Mongerはそんな陰湿ないじめはしませんし、Rubyistを公言してると社内で立場が危うくなるなんてことももちろんありませんし、そう言えばトゥシューズなんか履いたことないや。

比較の為に「彼女が404」を作ろうかと思ったんだけど、そのまんま同じのを作ってもあんまり芸がないので「うどん屋が403」にしてみた。彼女のと同じくGET、POST、PUT、DELETEに対応しててそれぞれ違うレスポンスを返すので、適当にリクエスト投げてみてくだしあ。

ファイルの構成はこんな感じ。

UdonApp.pm
うどん屋のステータスを返すアプリ。
UdonMap.pm
Rack::URLMapもどき。パスとアプリを関連付けてアプリのhandle_requestを呼ぶ。要Path::Dispatcher。
UdonHandler.pm
UdonMapを呼ぶためのmod_perl2用のハンドラ。PerlSetEnvでAPP_BASE_DIR(必須)とAPP_LOCATION(任意)を設定できる。APP_BASE_DIRはviewのディレクトリを決定するのに、APP_LOCATIONは例えば/perl/resource/udonみたいなパスでアクセスさせたいときに「PerlSetEnv /perl」みたいにして使う。
server.pl
実行するとスタンドアロンでサーバ立ち上げる。
server-switch.pl
server.plを「HTTP::Engine をつかった、ごくシンプルなプログラムの例(The simple example code for HTTP::Engine) - TokuLog 改めB日記」を参考に直したもの。要Perl5.10。なんで最初からそう書かなかったのかと言うと、Macの方のPerlが5.8なので…。given-whenの方が好みだなぁ。
server-urlmap.pl
server.plをUdonMapを使うように直したもの。これが一番すっきりするかなぁ。ていうかしないとUdonMapの意味無いけど。
udon.cgi
CGI。普通にApacheとかで動く。
*.msn
viewファイル。

ちょっと構成変えた。モジュールはUdon*.pmからlib/Udon/*.pmに、テンプレートは*.msnからview/*.msnに、スクリプトは*.plからbin/*.plに移動。

Ruby版の挙動になるべくあわせるようにしようとして、わざわざURLMapもどきを作ってしまった。Path::Dispatcherの無駄遣いな感が否めない。本当はPath::Dispatcherはもっと色々できる高機能なディスパッチャなので、WAFを作るときはUdonMapにあたるところがディスパッチャでUdonAppがアプリケーションじゃなくてコントローラ、的な構成にすると思うけど、まぁRackのプチアプリとの比較ってことでその辺は御愛嬌。

今思ったけど、これHTTP::Engine::Middlewareとかで実装すべきかしら。というかすでにこんなのあったりしそう。まだMiddlewareの方はちゃんと追えてないので後で。やってみた。次の記事参照。

ちなみにテンプレートにはText::MicroMason::SafeServerPagesを使った。普段使ってないテンプレートエンジンを試してみたかったってだけなので、特に意味はない。でも良いな、TTよりよっぽど素直に書けて好きだ。

参考にしたもの

↑このページのトップヘ