Perl Hackers Hub

第23回 Perlアプリケーションのテストと高速なCI環境構築術(1)

この記事を読むのに必要な時間:およそ 5 分

Plack::TestによるWebアプリケーションのテスト

Webアプリケーションのテストには,実際にプログラムをテストサーバにデプロイして行う方法,モックリクエストを使う方法など,さまざまな手法があります。近年ではPSGIとPlackの普及により,Webアプリケーションのプログラムとテストがより書きやすくなりました。アプリケーションがPlackベースの場合には,Plack::Testを用いると容易にテストを書くことができます。

use Plack::Test;
use Test::More;
use HTTP::Request;

subtest "WebAPP のテスト" => sub {
    my $app = sub {
        return [
            200,
            [ 'Content-Type', 'text/plain' ],
            [ "Hello" ]
        ];
    };

    test_psgi $app, sub {
        my $cb = shift;
        my $req = HTTP::Request->new(
            GET => 'http://localhost/'
        );
        my $res = $cb->($req);
        is $res->code, 200, '200 ok';
        is $res->content, 'Hello', 'body ok';
    };
};

done_testing;

上記の例はステータスコード200およびHelloが返されることを期待した処理です。

$appの部分が実際のWebアプリケーション実装になります。例では$appは単なるcoderefですが,実際には次に示すように,実装したアプリケーションのモジュールをテストする流れになります。

use Plack::Test;
use Test::More;
use HTTP::Request;
use MyAPP::Foo::Request;
use MyAPP::Foo::Logic;

my $app = sub {
    my $env = shift;
    $env->{HTTP_COOKIE} = "foo=var";
    my $req = MyApp::Foo::Request->new($env);
    my $logic = MyApp::Foo::Logic->new(req => $req);
    $logic->run;
};

subtest "MyAPP::Foo::Logic のレスポンステスト" => sub {
    test_psgi $app, sub {
        $cb = shift;
        my $res = $cb->(GET => '/path/to/app');

        is $res->code, 200;
        is $res->content, 'expected response';
    }
};

done_testing;

Test::mysqldによるMySQLを利用するテスト

MySQLを利用するテストの場合,Test::mysqldを用いると,テストごとにクリーンなMySQLを起動させることができます注1⁠。

このテストで起動したMySQLは$mysqldの参照スコープを抜けた段階で自動的に終了処理が行われ,テストで使われたデータベースがシャットダウンされます。

use DBI;
use Test::More;
use Test::mysqld;

my $mysqld = Test::mysqld->new(
    my_cnf => {
        'skip-networking' => '',
    }
) or plan skip_all => $Test::mysqld::errstr;

subtest "dbh を使うテスト" => sub {
    my $dbh = DBI->connect(
            $mysqld->dsn(dbname => 'test'));

    isa_ok $dbh, 'DBI::db', 'dbh is ok';

    # 以降に$dbh を利用するテスト
};

done_testing;

1つのテーブルのみを相手にする場合は上記のような形でもそれほど問題はありませんが,通常のアプリケーションでは複数のテーブルを利用することもあるでしょう。個別にDDLData Definition Languageを書いているとテストのメンテナンスが大変になるため,そういったときはテストの共通処理モジュールを作ってそちらに委譲するか,Test::Fixture::DBIなどの支援モジュールを用いることでテスト用データを容易に取り込むことができます。Test::Fixture::DBIの使い方については,本誌ムック『Perl徹底攻略』の記事注2を参照してください。

注1)
PostgreSQL用のTest::postgresqlもあります。
注2)
嶋田裕二(xaicron)「Webプログラミング」第6章「高速なWeb APIの実装とテスト ─⁠─ Mobage APIを支えるノウハウ」Perl徹底攻略⁠WEB+DB PRESS plus⁠⁠,技術評論社,2013年,pp.77-85

Test::Memcachedによるmemcachedを利用するテスト

Test::mysqld以外にも,ミドルウェアのテストモジュールはたくさん存在します。たとえばmemcachedであればTest::Memcachedを用いることで,テストごとにmemcachedを立ち上げることができます。

use Test::More;
use Test::Memcached;
use Cache::Memcached;

my $memd = Test::Memcached->new(
    options => { user => 'nobody' }
);
$memd->start;
my $port = $memd->option('tcp_port');

my $client = Cache::Memcached->new({
        servers => [ "127.0.0.1:$port" ]
    });

subtest "memcached を使うテスト" => sub {
    $client->set('key' => 'value');
    is $client->get('key'), 'value', 'value is ok';
};

$memd->stop;

done_testing;

memcached のほかにも,RedisであればTest::RedisServer など,さまざまなテストモジュールがCPANにリリースされているので,新しいミドルウェアを検討する際には探してみるとよいでしょう。

proveコマンドによるテストの実行と結果出力

ここまで,各種Perlアプリケーションにおけるテストの基本とその類型を紹介しました。本項では,それらのテストを実行する枠組みと結果出力について取り上げます。

テストが1つだけならば,そのモジュールをuseしてロジックを実行するPerlスクリプト1つあれば事足ります。しかしながら,開発していく中でテストファイルの数が増えていくのは必然です。そういったときにはproveコマンドを用いると,複数のテストをまとめて実行し,TAPTest Anything Protocol形式で結果を出力してくれます。

次の例ではtest_dir以下にあるテストスクリプトを実行し,結果を出力します。

$ prove -r /path/to/test_dir/
/path/to/test_dir/test1.t ................. ok
/path/to/test_dir/test2.t ................. ok
/path/to/test_dir/test3.t ................. ok
All tests successful.
Files=3, Tests=10, 9 wallclock secs ( 0.07 usr 0.02 sys +
5.94 cusr 0.62 csys = 6.65 CPU)
Result: PASS

このコマンドは裏でTAP::Harnessというモジュールを利用しています。

TAP::Harnessのしくみ

Perlのテスト実行を担うモジュールであるTAP::Harnessについて説明します。

TAP::HarnessはPerlのテストフレームワークで,proveコマンドで実際にテストを実行するモジュールです。このモジュールが持つruntestsメソッドにテストファイルの名前を渡すと,そのテストを実行します。

use TAP::Harness;

my $harness = TAP::Harness->new({
    verbosity => 1,
    lib => [ '/path/to/test_dir/lib', '/path/to/extlib' ],
});

my @tests = (
    '/path/to/test_dir/test1.t',
    '/path/to/test_dir/test2.t',
    '/path/to/test_dir/test3.t',
);
$harness->runtests(shuffle @tests);

このような形でPerlのテストを実行すると,先のproveコマンドと同様に,標準出力にテスト結果がTAP形式で出力されます。

Failed 0 / 30.
==========
/path/to/test_dir/test1.t .............................
/path/to/test_dir/test2.t ................................
/path/to/test_dir/test3.t ................................
Files=3, Tests=10, 9 wallclock secs ( 0.07 usr 0.02 sys +
5.94 cusr 0.62 csys = 6.65 CPU)
Result: PASS

このTAP::Harnessは後述する高速なCIクラスタの構築における中核モジュールとなるので,覚えておいてください。

<続きの(2)こちら。>

著者プロフィール

久森達郎(ひさもりたつろう)

株式会社フリークアウトの下回り系エンジニア。最近うっかり蒙古タンメン中本にハマってしまい,血が唐辛子色になりつつある。昨今のCasualムーブメントの一翼を担うMySQL Casualを運営。

YAPC::Asia 2012ではBest Talk Award 3位,YAPC::Asia 2013ではBest Talk Award 2位を受賞。

Twitter:@myfinder
blog:http://myfinder.hatenablog.com/