Mojoliciousの「自動試験」のテクニック
Mojoliciousの「自動試験」を行うためのテクニックをまとめました。
Mojolicious::Liteでスクリプトをデバッグする方法
Mojolicious::Liteではスクリプトを簡単にデバッグすることができます。とても短い記述で試験をかけるというのがうれしい点です。Mojolcious::LiteでWebアプリを作成する場合の大きな利点です。
まずはMojolicious::Liteで作成した簡単なWebアプリです。
use Mojolicious::Lite; get '/' => sub { my $self = shift; $DB::single = 1; $self->render(text => 'Hello'); }; app->start;
このファイルを「myapp.pl」という名前で保存してください。注目するポイントは「$DB::single = 1」という記述です。これはスクリプトの中にブレークポイントを埋め込むための記法です。デバッガはこの行を見つけるとその位置でストップします。
次に試験用のディレクトリを作成します。
mkdir t
tというディレクトリの中に試験用のスクリプトを配置します。このディレクトリの中に「basic.t」という名前のスクリプトを作成してみてください。
use Test::More 'no_plan'; use strict; use warnings; use utf8; use Test::Mojo; use FindBin; $ENV{MOJO_HOME} = "$FindBin::Bin/.."; require "$ENV{MOJO_HOME}/myapp.pl"; my $t = Test::Mojo->new; $t->get_ok('/');
(参考)FindBin
このスクリプトはMojolicious::LiteのPODに記述されている自動試験用のスクリプトですが、デバッグを行うためにも利用することができます。「$t->get_ok('/')」という記述で実際に「/」にアクセスした場合の処理が実行されるからです。
次にこのテストスクリプトををデバッガを使って起動しましょう。実行するディレクトリは、tディレクトリの中ではなくて、スクリプトのおいてあるディレクトリから行うのがよいでしょう。
perl -d t/basic.t
libディレクトリの中にモジュールを配置してある場合は次のようにlibディレクトリをモジュールの検索パスに追加してあげます。
perl -d -Ilib t/basic.t
デバッガが起動したら「c」コマンドで「$DB::single = 1」と記述したブレークポイントまで進みます。
c
デバッグをしたい位置に到着することができます。
5 6: $DB::single = 1; 7 8==> $self->render(text => 'Hello'); 9: }; 10 11: app->start;
このテクニックを覚えておけば、Mojolicious::Liteの開発はいっそう楽なものになります。
Mojoliciousでページのリンクが切れていないかをチェックするテストを書く
次のようなメソッドを定義します。これはページの内容を取得して、その中のハイパーリンクをすべて取得し、そのリンクにアクセスしたときに200が返ってくるとOKという試験です。use Test::Mojoした後に、記述してください。$Test::Builder::Levelを1増やしているのは、テストに失敗した行の呼び出しもとの情報を得るためです。
{ package Test::Mojo; sub link_ok { my ($t, $url) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; my @links; $t->ua->get($url)->res->dom->find('a[href]')->each(sub { my $self = shift; push @links, $self->attrs->{href}; }); for my $link (@links) { $t->get_ok($link)->status_is(200) unless $link =~ /logout/; } } }
利用するときはこんな感じです。
$t->link_ok('/user/list');
テストを作成するのは、面倒に感じるかもしれませんが、コードの変更にとても強く、安心感があります。
Mojoliciousの自動試験で試験が失敗した行を表示する
Mojoliciousで自動試験をしていると、出力の内容が大きいのでどこの行で試験が失敗したかとても見づらくて大変です。
パイプとgrepをうまく使うとその行番号だけを表示することができます。
perl myapp.pl test 2>&1 | grep line
試験結果は標準エラー出力に出力されるので、それをパイプを使ってgrepに流し込んでlineという行が含まれている行のみ取り出します。
自動試験中に警告が発生した場所を調べる
Mojoliciousで自動試験を行っていると以下のような未定義値が利用されていますというような警告がよく発生します。
Use of uninitialized value in concatenation (.) or string at (eval 520) line 94.
場所を特定して問題の場所を探さなければならないのですが、これを自力で行うのは大変です。このような場合は警告を例外に変換して、呼び出し元の情報を取得するのがよい方法です。警告を例外に変換するには以下のようにします。
# Convert warning to exeption use Carp 'confess'; $SIG{__WARN__} = sub { confess $_[0] };
上記のように記述すると警告が発生したときのコールバックを記述することができます。confessは例外を発生させて、メッセージに呼び出し元を含むスタックとレースを含めることができます。これは自動試験のスクリプトの上のほうで記述しておくとよいでしょう。
これでスクリプトでの行番号とどのような処理を行っているのかがわかるので、その処理が出力しているデータをダンプすると原因がわかる場合が多いでしょう。
Mojoliciousの試験中に無限ループで固まった場合の対処方法
Mojoliciousで開発していると、同名のメソッドを間違って呼び出したために、無限ループに陥ってしまうということがときどきあります。Ctrl + cが聞かなくなりサーバをとめられなくなってしまいます。
このような場合は、いったんCtrl + zを押してサーバーをバックグラウンド実行に切り替えます。その後にpsコマンドでサーバのプロセスIDを特定し、-KILLシグナルで、プロセスを強制的にとめます。
ps -ef | grep myapp.pl kill -KILL プロセスID
追記
以下のような意見があったので補足。
mojoliciousはctrl+cしないといけない事が多いってのは、コントローラとビューでそれぞれ余計な事やっちゃってる説
これはMojoliciousが原因ではなくて、コントローラクラスの一部の機能をベースクラスに分離したときに、スーパークラスのメソッドを呼び出すのをわすれて、自身と同名のメソッドを呼び出してしまって無限ループという一般的なオブジェクト指向で間違いのことをいっています。通常のプログラミングだと、無限ループが発生するとプログラムがエラーで終了するわけですけれど、サーバーの場合はとめられなくなるので、解決策を書いているわけで、Mojoliciousが変なことやっているわけではないです。
自動試験の中で発生した例外を知る方法
Mojoliciousで自動試験を書いているときに、試験がうまくいかなくてテンプレートにServer errorと表示され例外がおきたことだけが、わかることって結構ありますね。こういうときは、どうやって例外の内容を知ったらいいのだろうかと悩みます。
それで、ひとつの方法をお知らせしておきますね。
例外の内容を捕獲して出力する
例外の内容はスタッシュのexceptionというキーから取得できます。フックを仕掛けてこれを取得してしまいましょう。
my $app = YourApp->new; my $t = Test::Mojo->new($app); $app->hook(after_dispatch => sub { my $c = shift; my $exception= $c->stash('exception'); warn $exception if $exception; });
testコマンドで試験の詳細を表示する
verboseオプションを使うと、testコマンドで詳細を表示することができます。
perl myapp.pl test --verbose
テスト用途でSSLで接続できるサーバーを起動する
SSLの試験をしたい場合があると思います。Mojoliciousでは試験用の秘密鍵とサーバー証明書を持っているので、httpsで接続するサーバーを起動することができます。前提条件としてIO::Socket::SSLというモジュールが必要になりますのでインストールしておきます。
cpan IO::Socket::SSL
次のようなオプションでサーバーを起動するとhttpsで接続を行うことができます。
# 組み込みのスタンドアロンサーバー perl myapp.pl daemon --listen https://*:3000 # 自動リロードしてくれる試験用のmorboサーバー morbo myapp.pl --listen https://*:3000
(参考)morbo
次のようなURLでhttpsで接続することが可能になります。
https://localhost:3000
もちろんHTTPとHTTPSの両方でリッスンするのが普通なので、次のようにするのがよいでしょう。
morbo myapp.pl --listen http://*:3000 --listen https://*:3001
SSLというのはポートが指定されない場合は、デフォルトで443番ポートに接続することが想定されます。でも試験環境では、違うポートでSSLへのリクエストをリッスンしますね。
でもこうしちゃうよりも環境変数を設定することがお勧めです。こうしておくと、ユーザーごとに別ポートを利用して試験ができるようになります。
export MOJO_LISTEN=http://*:3000,https://*:3001
でもこれだと本番機と開発機でURLに関して別のソースコードを書かないといけなくなったりして、面倒ですね。そこで次のようなヘルパーを作成しておくのがよいでしょう。ポート番号が存在する場合は、それに1を足したポート番号がSSLのポート番号とするように、運用で決めておきます。
また本番では、ポートは指定せずに、デフォルトに任せて、HTTPは80番、HTTPSは443番とします。
次のようなヘルパーを登録しておけば、汎用的にできますね。
$app->helper( url_for_https => sub { my $self = shift; my $url = $self->url_for(@_)->to_abs->scheme('https'); my $port = $url->port; $url->port($port + 1) if defined $port; return $url; } );
CSS3セレクタを使ってHTMLの要素が存在するかを確認する
Test::Mojoのelement_existsメソッドを使用すれば、CSS3セレクタに一致するHTMLの要素が存在するかを試験することができます。
$t = $t->element_exists('div.foo[x=y]'); $t = $t->element_exists('html head title', 'has a title');
Test::Mojoのelement_exists_notメソッドを使用すれば、CSS3セレクタに一致するHTMLの要素が存在しないこと試験することができます。
$t = $t->element_exists_not('div.foo[x=y]'); $t = $t->element_exists_not('html head title', 'has no title');
CSS3セレクタを使ってHTMLの要素のテキスト内容を試験する
Test::Mojoのtext_isメソッドを使用すれば、CSS3セレクタに一致するHTMLの要素のテキスト内容を試験することができます。
$t = $t->text_is('div.foo[x=y]' => 'Hello!'); $t = $t->text_is('html head title' => 'Hello!', 'right title');
Test::Mojoのtext_isntメソッドを使用すれば、CSS3セレクタに一致しないHTMLの要素のテキスト内容を試験することができます。
$t = $t->text_isnt('div.foo[x=y]' => 'Hello!'); $t = $t->text_isnt('html head title' => 'Hello!', 'different title');
Test::Mojoのtext_likeメソッドを使用すれば、CSS3セレクタにマッチするHTMLの要素のテキスト内容を試験することができます。
$t = $t->text_like('div.foo[x=y]' => qr/Hello/); $t = $t->text_like('html head title' => qr/Hello/, 'right title');
またTest::Mojoのtext_unlikeメソッドを使用すれば、CSS3セレクタにマッチしないするHTMLの要素のテキスト内容を試験することができます。
$t = $t->text_unlike('div.foo[x=y]' => qr/Hello/); $t = $t->text_unlike('html head title' => qr/Hello/, 'different title');