Mojoを使って自作ウェブアプリをよりポータブルに!

第2回Mojoを使えばテストも簡単!

2009年12月8日:サンプルコードを現在のバージョンで動作するよう修正しました。

一行掲示板を移植してみよう

連載の1回目では、Mojoのスタンドアロンサーバを使って簡単な画面を表示してみました。今回は簡単なCGIをMojoに移植しながら、リクエストの扱い方やテストの仕方を見ていきましょう。

まずはこれから移植していくCGIのソースを掲載します。あきらかに穴だらけのものですが、そこはひとまず目をつむってください。

#!/usr/bin/perl
use strict;
use warnings;
use CGI;

my $q = CGI->new;

my $file = 'data.txt';
unless (-f $file) {
    open my $fh, '>', $file or die $!;
    close $fh;
}
if ($q->param('text')) {
    open my $fh, '>>', $file or die $!;
    print $fh $q->param('text'), "\n";
    close $fh;
}
open my $fh, '<', $file or die $!;
my @lines = <$fh>;
close $fh;

print "Status: 200 OK\n";
print "Content-Type: text/html; charset=utf-8\n\n";
print <<"END";
<html><head><title>Simple BBS</title></head><body>
<form method="POST"><input type="text" name="text"></form>
@{[ map{ "<p>$_</p>" } @lines ]}
</body></html>
END

最後のヒアドキュメントのなかにPerlの式を埋め込むテクニックはあまりなじみがないものかもしれません。詳しくは竹迫良範さんがJPerl Advent Calendar 2008に寄稿された記事[1]をご覧ください。

書き直すのはリクエストとレスポンスだけ

さて、これをそのままMojoで書き直してみます。

まずは連載1回目と同じく、Mojoをインストールしたディレクトリに新しいひな形を用意しましょう。

> perl script/mojo generate app SimpleBBS
> cd simple_bbs

続いて、lib/SimpleBBS.pmの中身をこう書き換えます。

package SimpleBBS;

use strict;
use warnings;

use base 'Mojo';

sub handler {
    my ($self, $tx) = @_;

    my $file = 'data.txt';
    unless (-f $file) {
        open my $fh, '>', $file or die $!;
        close $fh;
    }
    if ($tx->req->param('text')) {
        open my $fh, '>>', $file or die $!;
        print $fh $tx->req->param('text'), "\n";
        close $fh;
    }
    open my $fh, '<', $file or die $!;
    my @lines = <$fh>;
    close $fh;

    $tx->res->code(200);
    $tx->res->headers->content_type('text/html; charset=utf-8');
    $tx->res->body(<<"END");
<html><head><title>Simple BBS</title></head><body>
<form method="POST"><input type="text" name="text"></form>
@{[ map{ "<p>$_</p>" } @lines ]}
</body></html>
END

    return $tx;
}

1;

連載第1回でも説明した通り、全体がhandlerというサブルーチンの中に記述されていることを除けば、変わったのはCGIオブジェクトのかわりに$tx->reqからアクセスできるMojoのリクエストオブジェクトが使われていることと、出力部分が$tx->resからアクセスできるレスポンスオブジェクトに格納されていることだけです。簡単ですね?

リクエストオブジェクトの中身

このリクエストオブジェクトの正体は、Mojo::Message::Requestのインスタンスです。ここにはさまざまなメソッド、オブジェクトが格納されていますが、ふだんよく使うのはこのくらいでしょうか。

$tx->req->param(パラメータ名)

クエリストリングやフォームデータのパラメータを返します。

$tx->req->method

リクエストのメソッド(GET、POSTなど)を返します。

$tx->req->is_multipart

リクエストがマルチパートのフォームデータかどうかを返します。

$tx->req->cookie(クッキー名)->value

クッキーの値を返します。

$tx->req->headers->content_length

Content-Lengthヘッダの値を返します。

$tx->req->headers->content_type

Content-Typeヘッダの値を返します。

$tx->req->headers->header(ヘッダ名)

任意のヘッダの値を返します。

$tx->req->url->path

PATH_INFOの値を返します。

$tx->req->upload(パラメータ名)->file->move_to(移動先)

アップロードされたファイルを移動します。

そのほかのメソッドや、各メソッドの詳細については該当するクラスのPODやソースコードをご覧ください。

CGIとして動かしてみる

さて、このSimpleBBS、連載第1回のように開発用のスタンドアロンサーバで動かしてもよいのですが、お手元にApacheなどのサーバがあるならCGIとして動作させてみましょう。

本当はシェル(コマンドライン)から「perl script/simple_bbs cgi」を実行するとCGI向けの出力が得られるのですが、この形ではブラウザから実行しづらいので、別に起動スクリプト(index.cgi)を用意します(use libの中身はお手元の環境にあわせて適宜修正してください⁠⁠。

#!/usr/bin/perl
use strict;
use warnings;
use lib qw(
  /home/ishigaki/mojo/lib
  /home/ishigaki/mojo/simple_bbs/lib
);
use Mojo::Server::CGI;

$ENV{MOJO_APP} = 'SimpleBBS';

Mojo::Server::CGI->new->run;

サーバの設定ファイルにCGIの実行権限を追加して再起動したら、ブラウザからいまのCGIを実行してみてください。送信したテキストがテキストボックスの下に表示されていけばひとまず成功です。

テストを書こう

ところで、最初にお断りしたように、このCGIにはいくつもバグがあります。たとえば、テキストボックスから半角の0を送信してみてください。本当は0というテキストが追加されるべきなのに、なにも起こらなかったはずです。

このような場合、みなさんならどうするでしょうか?

この程度の分量であれば、いきなりソースコードを見ても問題なく直せるでしょう。でも、コードがもっと複雑になってきた場合、そして同じようなフォームがたくさんある場合、いきなりコードに手を入れるのは下策です。ここでやってしまったケアレスミスをほかのところで繰り返さない保証はないのですから、せめてテスト項目に加えておくべきですし、できれば自動的にテストできるようテストコードを書きたいところです。

ウェブアプリケーションをテストする場合、一般的にはWWW::Mechanizeのようなモジュールを使って本番環境と同じ動作をする検証用アプリケーションにアクセスし、フォームに値を入れたりリンクをたどったりしていくのが常道ですが、Mojoの場合、テストにあたってわざわざ検証用の環境を構築する必要はありません。たとえば、0に対するテストであれば、このように書けます。

use strict;
use warnings;
use Test::More tests => 1;
use SimpleBBS;
use Mojo::Transaction::Single;

my $app = SimpleBBS->new;
{
    my $tx = Mojo::Transaction::Single->new;
    $tx->req->param('text' => 0);
    $app->handler($tx);
    like $tx->res->body => qr{<p>0</p>};
}

テストを実行してみる

このテストをt/false_value.tという名前で保存したら、実際にテストを走らせてみましょう。シェル(コマンドライン)からこのようにタイプしてください。

> perl script/simple_bbs test

いまの段階では当然テストは失敗します。問題の箇所を修正しましょう。差分はこうなります。

@@ -13,7 +13,7 @@
         open my $fh, '>', $file or die $!;
         close $fh;
     }
-    if ($tx->req->param('text')) {
+    if (defined $tx->req->param('text')) {
         open my $fh, '>>', $file or die $!;
         print $fh $tx->req->param('text'), "\n";
         close $fh;

テストは正常に見えるバグも見つけてくれます

これでブラウザからは直ったように見えますが、ふたたびテストを実行してみると、まだミスが見つかります。

Running tests from '/home/ishigaki/mojo/simple_bbs/t'.
t/basic..........ok
t/false_value....1/1
#   Failed test at t/false_value.t line 12.
#                   '<html><head><title>Simple BBS</title></head><body>
# <form method="POST"><input type="text" name="text"></form>
# <p>0
# </p>
# </body></html>
# '
#     doesn't match '(?-xism:<p>0</p>)'
# Looks like you failed 1 test of 1.
t/false_value.... Dubious, test returned 1 (wstat 256, 0x100)
 Failed 1/1 subtests

改行が落ちていなかったのですね。

@@ -19,7 +19,7 @@
         close $fh;
     }
     open my $fh, '<', $file or die $!;
-    my @lines = <$fh>;
+    my @lines = map { chomp; $_ } <$fh>;
     close $fh;

     $tx->res->code(200);

これでひとまずt/false_value.tのテストは通るようになりました。めでたしめでたしと言いたいところですが、このテストデータはいったいどこに保存されていたのでしょうか?

設定は外から変更できるように

テストを実行したときに、前のテストデータが残っていては正しいテストにはなりません。データファイルの設定は外から変更できたほうがよいでしょう。

このような場合、アプリケーションのコンストラクタに環境依存の設定を渡せるようにするのが常道ですが、ここではあとからデータファイルを操作することが予想されますので、アクセサ/ミューテータを用意することにします。

lib/SimpleBBS.pmの差分はこのようになります。

@@ -5,10 +5,12 @@

 use base 'Mojo';

+__PACKAGE__->attr(datafile => 'data.txt');
+
 sub handler {
     my ($self, $tx) = @_;

-    my $file = 'data.txt';
+    my $file = $self->datafile;
     unless (-f $file) {
         open my $fh, '>', $file or die $!;
         close $fh;

t/false_value.tの方も修正しましょう。

@@ -6,8 +6,10 @@

 my $app = SimpleBBS->new;
 {
+    $app->datafile('test_false_value.txt');
     my $tx = Mojo::Transaction::Single->new;
     $tx->req->param('text' => 0);
     $app->handler($tx);
     like $tx->res->body => qr{<p>0</p>};
+    unlink $app->datafile;
 }

これで何度テストを実行しても、古いデータでテストしたり本番環境のデータを壊したりすることはなくなりました。

同じ要領でt/xss.tを書いて、XSS(クロスサイトスクリプティング)のバグもつぶしておいてください。

Mojoを使うと、アプリケーションが自然にサーバ環境に依存する部分とそうでない部分に分かれていきます。アプリケーションと各種サーバを結びつける部分はMojoのほうでテスト済みですから、わさわざ自分でテストする必要はありません。どのような環境でも変わらない、アプリケーション本体だけをテストすればよい――そのような責任の分担が、Mojoを使う大きなメリットのひとつです。

次回は画面遷移のあるもう少し複雑なアプリケーションに挑戦してみます。お楽しみに。

おすすめ記事

記事・ニュース一覧