N10K 掲示板のテストの一部

N10K 掲示板のテストは、手抜きで結合テストだけになっています。単体テストは書いておらず、HTTP リクエストに対する HTTP レスポンスの要件を Test::Mojo で長々と書き連ねてあります。日本語のメッセージをポストしてレスポンスで受け取るテストになので、テストの冒頭で標準出力とエラー出力のハンドラのエンコーディングを設定しています。

use strict;
use warnings;
use utf8;
BEGIN{ binmode STDOUT, ':utf8'; binmode STDERR, ':utf8'; }
use Encode;
use Test::More;
use Test::Mojo;

続いて、n10k をロードし、データファイルを削除して、掲示板オブジェクトの設定をテスト向けのものにします。

use FindBin;
require "$FindBin::Bin/../n10k.pl";

my $datadir = "$FindBin::Bin/../data";
if (-d $datadir) {
    -e "$datadir/entries.data" and unlink "$datadir/entries.data";
    -e "$datadir/entries.index" and unlink "$datadir/entries.index";
    -e "$datadir/entries.log" and unlink "$datadir/entries.log";
}

{
    my $board = N10k::Controller->board;
    $board->config->{'top_entries'} = 10;
    $board->config->{'per_page'} = 10;
    $board->config->{'pager_width'} = 9;
    $board->object->config->{'appkey'} = 'E1ib23.5kjd/eQcfBtE-TeHrepo3krewnKHJee0etk';
}

これで準備ができたので、Test::Mojo を使って、空のトップページが取得できているかどうかをチェックします。投稿用のフォームが存在し、その中に入力するべき項目が定義されていることを調べます。空なので、エントリがなく、個数表示もゼロになっていることを確かめます。この辺は、Mojo::DOM が CSS セレクタに対応しているので、わかりやすく記述できます。

my $t = Test::Mojo->new;
$t->get_ok('/')
  ->status_is(200)
  ->element_exists('form[method="POST"][action="/"]')
  ->element_exists('form textarea[name="source"]')
  ->element_exists('form input[name="signature"]')
  ->element_exists('form input[type="submit"]')
  ->element_exists_not('div.entry')
  ->text_is('nav#pager .entry-count' => 0);

続いて、最初の投稿をおこないます。 リクエストメソッド POST でフォームを投稿するときは、 post_form_ok を使います。 JSON で投稿するときは、 post_json_ok を使います。 リクエスト・ボディを明示的にテキストで渡すときは post_ok を使います。post_form_ok は最後の引数にヘッディング定義を受け取ることができ、マルチパートで POST したいときは、 そこに Content-Type を記述します。 utf8 フラグがオンのスカラーを渡すときは、 UTF-8 指定が必要です。レスポンスの Mojo::DOM では文字列は decode 済みなので、 utf8 フラグの立ったスカラーを比較にそのまま利用することができます。

my $header = {'Content-Type' => 'multipart/form-data'};
my $form1 = {
    'source' => q(1 番目。テスト中。)."\n".q(&<>"'。),
    'signature' => "",
};
$t->post_form_ok('/', 'UTF-8', $form1, $header)
  ->status_is(200)
  ->element_exists('div.entry#e8')
  ->element_exists('div.entry#e8 .entry-body')
  ->element_exists_not('div.entry#e8 .entry-footer .signature')
  ->element_exists('div.entry#e8 .entry-footer .remote')
  ->element_exists('div.entry#e8 .entry-footer .posted a[href="/entry/8"]')
  ->element_exists('#pager')
  ->element_exists_not('#pager-list a')
  ->text_unlike('#pager-list' => qr/[|]/msx)
  ->text_is('#pager-list strong' => 'トップ')
  ->text_is('nav#pager .entry-count' => 1);

Mojo::DOM はエンティティをデコードしてしまうため、アプリケーションがどのようにエスケープをおこなったのか調べたいときは、レスポンス・ボディを調べます。レスポンス・ボディは decode 前のオクテットのままなので、まず decode してから、正規表現で欲しい箇所を抜き出して、比較テストをしています。

{
    my $body = decode_utf8($t->tx->res->body);
    my($div) = $body =~ m{\Q<div class="entry" id="e8">\E(.*?)</div>}msx;
    my($p) = $div =~ m{\Q<p class="entry-body">\E(.*?)</p>}msx;
    is $p,
       q(1&ensp;番目。テスト中。)."<br />\n".q(&amp;&lt;&gt;&quot;&#39;。),
       'element for div[id] .entry-body escaped.';
}

2番目の投稿も同じようにテストします。今度はレスポンス・ボディのエスケープ済みかどうかのチェックを省いています。ここで、エントリボディの文字列チェックのテストですが、Test::Mojo は、 Mojo::DOM の text メソッドを使うので、上にあるようにボディでは ensp エンティティなのが空白文字へデコードされています。

my $form2 = {
    'source' => q(2 番目。テスト中。),
    'signature' => "Test::Mojo is useful",
};
$t->post_form_ok('/', 'UTF-8', $form2, $header)
  ->status_is(200)
  ->element_exists('div#e103')
  ->text_is('div#e103 .entry-body' => q(2 番目。テスト中。))
  ->text_is('div#e103 .entry-footer .signature' => 'ggcWlcryUJJ0')
  ->element_exists('div#e103 .entry-footer .remote')
  ->element_exists('div#e103 .entry-footer .posted a[href="/entry/103"]')
  ->element_exists('div#e8')
  ->element_exists('#pager')
  ->element_exists_not('#pager-list a')
  ->text_unlike('#pager-list' => qr/[|]/msx)
  ->text_is('#pager-list strong' => 'トップ')
  ->text_is('nav#pager .entry-count' => 2);

12番目まで同じように投稿していきます。途中略して、12 番目は次のようになります。トップページから最古の2つのエントリが追い出されて、最新の 10 エントリが残ること、ページャに /page/1 へのリンクが現れています。

my $form12 = {
    'source' => q(12 番目。テスト中。),
    'signature' => "Test::Mojo is useful",
};
$t->post_form_ok('/', 'UTF-8', $form12, $header)
  ->status_is(200)
  ->element_exists('div#e1106')
  ->text_is('div#e1106 .entry-body' => q(12 番目。テスト中。))
  ->element_exists('div#e1106 .entry-footer .posted a[href="/entry/1106"]')
  ->element_exists('div#e1004')
  ->element_exists('div#e903')
  ->element_exists('div#e803')
  ->element_exists('div#e703')
  ->element_exists('div#e603')
  ->element_exists('div#e503')
  ->element_exists('div#e403')
  ->element_exists('div#e303')
  ->element_exists('div#e203')
  ->element_exists_not('div#e103')
  ->element_exists_not('div#e8')
  ->element_exists('#pager')
  ->element_exists('#pager-list a[href="/page/1"]')
  ->text_like('#pager-list' => qr/[|]/msx)
  ->text_is('#pager-list strong' => 'トップ')
  ->text_is('nav#pager .entry-count' => 12);

一方、 /page/1 には、追い出された2つのエントリとトップへのリンクがなければいけません。

$t->get_ok('/page/1')
  ->status_is(200)
  ->element_exists_not('form[method="POST"][action="/"]')
  ->element_exists_not('div#e1106')
  ->element_exists_not('div#e1004')
  ->element_exists_not('div#e903')
  ->element_exists_not('div#e803')
  ->element_exists_not('div#e703')
  ->element_exists_not('div#e603')
  ->element_exists_not('div#e503')
  ->element_exists_not('div#e403')
  ->element_exists_not('div#e303')
  ->element_exists_not('div#e203')
  ->element_exists('div#e103')
  ->element_exists('div#e8')
  ->text_is('#pager-list strong' => '1')
  ->text_like('#pager-list' => qr/[|]/msx)
  ->element_exists('#pager-list a[href="/"]')
  ->text_is('nav#pager .entry-count' => 12);

最後に、Test::More を使うときのお約束の一行を書きます。

done_testing;

テスト実行は Mojolicious::Lite アプリケーションでおこないます。

$ perl n10k.pl test t/01.post.t