ウェブアプリケーションのテストの仕組みを読み解こう

※本記事は、旧 Tech Talk Blog 内の 「ウェブアプリケーションのテストの仕組みを読み解こう (http://www.sixapart.jp/techtalk/2006/10/dev-vox-test.html) はてなブックマーク - 」 で公開されていた記事を移転しました。

はじめまして。Vox 開発エンジニアの谷本です。

突然ですが、Vox の開発はいつから始まったのでしょうか。当ブログの以前のエントリーにもありますが、Vox はプレビュー版として3月にサービスが開始されていることからもわかるように、開発は去年から行われていました。となると、もうそれなりに期間の長いプロジェクトですし、アプリケーションの規模でも今では弊社の他の製品に引けを取らないほどになっています。

私が Vox の開発に加わったのは今年の8月なのですが、既に大きく成長していたアプリケーションを前に、最初は「Vox を壊してしまわないだろうか...」という不安を抱えていました。しかし大量のテストコードのおかげで既存のアプリケーションを壊さずに、簡単、確実に機能を追加していくことができたのです。(実際にはテストのおかげで壊れているのが発覚したりしたのですが...)

前置きはこれぐらいにして、Vox は Perl 5 - Powering Web 2.0 でも紹介されているように、フレームワークに Catalyst を採用しています。Catalyst には Catalyst::Test というテスト用のモジュールが同梱されており、httpd を起動しなくてもウェブアプリケーションのテストを実行できるようになっています。(実際に httpd を起動してテストすることもできます)でも httpd を起動しないでウェブアプリケーションのテストをするって一体どういうことなんでしょうか。自分では「まぁなんとなくあんな感じ」という程度で、実際にどのような仕組みで動いているのかまで確認したことはありませんでした。弊社では独自のテスト用フレームワークを使っていますが、CPAN にある Test::WWW::Mechanize::Catalyst と仕組み的にあまり違いはありません。今回はこのモジュールの実装からその仕組みを理解したいと思います。

テストコードの先頭では、以下のようにして準備を行います。 MyApp は Catalyst で作成したアプリケーションのクラス名です。

 1: use Test::WWW::Mechanize::Catalyst 'MyApp';
2: my $mech = Test::WWW::Mechanize::Catalyst->new;

Test::WWW::Mechanize::Catalyst には _make_request() というメソッドが定義されており、WWW::Mechanize::_make_request() をオーバーライドしています。

Test::WWW::Mechanize::Catalyst::_make_request
1: sub _make_request {
2:     my ( $self, $request ) = @_;
3:     $self->cookie_jar->add_cookie_header($request) if $self->cookie_jar;
4:     my $response = Test::WWW::Mechanize::Catalyst::Aux::request($request);
5:     $response->header( 'Content-Base', $request->uri );
6:     $response->request($request);
7:     $self->cookie_jar->extract_cookies($response) if $self->cookie_jar;
8:     .....
9:     return $response;
10: }

WWW::Mechanize:: _make_request() は $mech->get('http://www.example.com/') のようにコンテンツを取得するメソッドが呼ばれた時に、LWP::UserAgent::request() を呼んで実際に HTTP リクエストの送信とレスポンスの処理を行います。しかしここでは、LWP ではなく Test::WWW::Mechanize::Catalyst::Aux::request() が呼ばれています。(4行目)
実はこの ::AUX::request の実体は Catalyst::Test::local_request() で、この中で擬似的に生成した HTTP クライアント・サーバの仕組みを使ってアプリケーションを実行しています。(httpd を起動したテストを行う場合は Catalyst::Test::remote_request() が使われ、こちらでは LWP が使われます)

Test::Catalyst::local_request
1: sub local_request {
2:     my $class = shift;
3:
4:     require HTTP::Request::AsCGI;
5:
6:     my $request = Catalyst::Utils::request( shift(@_) );
7:     my $cgi     = HTTP::Request::AsCGI->new( $request, %ENV )->setup;
8:
9:     $class->handle_request;
10:
11:     return $cgi->restore->response;
12: }

Catalyst::Utils::request() は URI などから HTTP::Request のインスタンスを生成して返します。(6行目)

今回の場合は LWP::UserAgent が生成した HTTP::Request のインスタンスが渡るので、何もせずにそのまま返されます。
HTTP::Request::AsCGI は STDIN や STDOUT などをテンポラリファイルへのディスクリプタに置き換え、Content-Length があると、STDIN に置き換えられているテンポラリファイルにメッセージボディを書き出しておきます。また、HTTP::Request のインスタンスの情報などから環境変数の設定を行います。(7行目)ここまでで HTTP リクエストを送信してサーバからのレスポンスを待っているような状態になっています。

次に呼ばれるのが MyApp(Catalyst)::handle_request() です。ここで先ほど設定した環境変数や STDIN からクライアントの情報を取得し Catalyst でおなじみのサイクル(下記)で処理が行われ、返したレスポンスは STDOUT に紐づいたテンポラリファイルへと出力されます。(9行目)

  prepare
dispatch
finalize

最後に、ディスクリプタの設定を元に戻し、テンポラリファイルの中身から HTTP::Response のインスタンスを生成して返します。(11行目)

まさにここで、実際のリクエストのやり取りが、どのようにシミュレートされているのかがわかりました。HTTP::Request::AsCGI 便利ですね。このような仕組みを理解しておくことで、テストやテスト用のフレームワークを書く時の参考になると思います。

最後にちょっとした Tips を紹介して終わりにしたいと思います。
最近は携帯端末でも XHTML に対応したブラウザがほとんどと言ってもいいと思います。携帯端末で XHTML を認識させるには、レスポンスヘッダに Content-Type: application/xhtml+xml をいれてブラウザに返す必要がありますが、WWW::Mechanize は text/html でないと、保持しているページコンテンツの中身を更新してくれず、また、自分で $mech->update_html() を呼ぶと中で保持している Content-Type を 'text/html' で上書きしてしまいます。

以下のようなコードを入れておけば、text/html の場合と同じ感覚でテストが書けるようになって少しだけ便利かもしれません。

{
no warnings 'redefine'; ## 利用される場合はご注意ください
*WWW::Mechanize::is_html = sub {
my $self = shift;
return defined $self->{ct} &&
($self->{ct} eq "text/html" || $self->{ct} eq 'application/xhtml+xml');
};
my $update_html = \&WWW::Mechanize::update_html;
*WWW::Mechanize::update_html = sub {
my $self = shift;
my $ct = $self->ct;
$self->$update_html(@_);
$self->{ct} = $ct;
};
}

Six Apart をフォローしませんか?

次の記事へ

MogileFS::Client と MogileFS 内部でのファイルノード管理

前の記事へ

MogileFS のインストールと初期設定