Perl Hackers Hub

第9回 高速なWeb APIの実装とテスト―Mobage APIを支えるノウハウ(2)

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

memcachedを使ったテストの書き方

DBと同様にmemcachedもテストのたびに立ち上げて,データがある状態とない状態での挙動などをテストしたほうがよいでしょう。

memcachedをテストごとに立ち上げるためにはProc::GuardとTest::TCPを使用します。リスト6のようにmemcachedを立ち上げるモジュールを書いておくと便利でしょう。これを使うと,ローカル環境でmemcachedが起動し,$procがスコープを抜けた段階で自動的に停止します。テストでは,この起動したmemcachedにアクセスするしくみを用意しておくとよいでしょう。

リスト6 テ スト用のmemcachedを立ち上げるモジュール(t/lib/Test/MyApp/Memcached.pm)

package Test::MyApp::Memcached;
use Proc::Guard qw/proc_gurad/;
use Test::TCP qw/empty_port/;
use File::Which qw/which/;

sub start_memd {
    my $port = empty_port();
    my $proc = proc_guard(
        scalar(which 'memcached'), '-p', $port);
    wait_port($port);
    return $proc, $port;
}

このようにテストごとに新しいmemcachedを立ち上げることで,いつでもクリーンな状態でテストが行えます。もちろんこの方法を使えば,memcached以外のあらゆる外部プログラムの立ち上げが可能になります。

make testを高速化!

テストが増えると,すべてのテストを走らせるのに時間がかかるようになります。ここでは,make testを高速に処理させる方法を紹介します。

MySQLをあらかじめ立ち上げておく

Test::mysqldを使ったテストの場合,1つのテストごとにMySQLを立ち上げて終了させるため,DBのテストが増えてくると極端に遅くなります。そこでMobage APIでは,

  • 1つずつテストを走らせるときは通常どおりMySQLを立ち上げる
  • make testでまとめて実行する場合は,はじめの処理でMySQLを立ち上げておいて,すべてのテストが終わったら終了する

というようにしています。まず,リスト4で書いたTest::MyApp::Fixture::DBIのstart_mysql()を,リスト7のように,すでにMySQLが立ち上がっている場合は使い回すように書き換えます。

リスト7  Test::MyApp::Fixture::DBIの修正(t/lib/Test/MyApp/Fixture/DBI.pm)

package Test::MyApp::Fixture::DBI;
use JSON;
use DBI;
use Test::mysql;

our $SKIP_DROP_DB_MAP = {
    information_schema => 1,
    mysql => 1,
    test => 1,
};

sub start_mysql {
    my %config = @_;

    my $mysqld;
    if (my $json = $ENV{TEST_MYSQLD}) {…①
        my $obj = decode_json $json;
        $mysqld = bless $obj, 'Test::mysqld';…②
        cleanup($mysqld);…③
    }
    else {
        $mysqld = Test::mysqld->new(my_cnf => +{ ―┐
            'skip-networking' => '', %config,      …④
        });                              ―――――――――┘
    }

    return $mysqld;

}

sub cleanup {
    my ($mysqld) = @_;
    my $dbh = DBI->connect($mysqld->dsn, '', '' {
        AutoCommit => 1, RaiseError => 1,
    });

    my $rs = $dbh->selectall_hashref(
        'SHOW DATABASES', 'Database');
    for my $dbname (keys %$rs) {
        next if $SKIP_DROP_DB_MAP->{$dbname};
        $dbh->do("DROP DATABASE $dbname");
    }
}
...

の$ENV{TEST_MYSQLD}にはTest::mysqldのオブジェクトをJSON形式にした値が入っています。もしその値があればすでにMySQLが立ち上がっているものとして,でJSONの値からTest::mysqldのオブジェクトを復元します。そのあとで,DBの中身をすべて削除し,クリーンな状態に戻します。$ENV{TEST_MYSQLD}に値がなければ,で今までどおり新規にMySQLを立ち上げます。

make testのときにMySQLを立ち上げるには,拙作のModule::Install::TestTargetを使うと簡単に書くことができます。リスト8のようにdefault_test_target()という関数のrun_on_prepareにMySQLを起動するPerlスクリプトを指定すると,make testを走らせたときにリスト9が実行され,MySQLが起動します。

リスト8  Module::Install::TestTargetを使用し,make testをフックする(Makele.PL)

use inc::Module::Install;
use Module::Install::TestTarget;
...
default_test_target(
    includes => ['t/lib'],
    run_on_prepare => ['t/script/setup_mysqld.pl'],
);
...

リスト9 MySQLを起動し,$ENV{TEST_MYSQLD}を設定する(t/script/setup_mysqld.pl)

use Test::MyApp::Fixture::DBI;
use JSON;

$SIG{INT} = sub { CORE::exit 1 };
$mysqld = setup_mysqld();
$ENV{TEST_MYSQLD} = encode_json +{ %$mysqld };

これで,make testを実行したとき,$ENV{TEST_MYSQLD}にTest::mysqldのオブジェクトをJSONにしたものが入るので,テストコードを修正することなく起動しているMySQLを使うようになるため,高速にテストを走らせることができます。

できるだけテストを並列で実行する

DBのテストは速くなりましたが,ほかのテストはどうでしょうか? 実は,あまり知られていないのですが,$ENV{HARNESS_OPTIONS}にj数字という文字列を入れると,テストが並列で実行されるようになります。たとえばj4とすれば4つのテストが並列に実行されるので,現実に4倍とはいかないまでも,かなり高速にテストが終わるようになります注13⁠。

この設定を有効にするには,リスト8をリスト10のように修正します。

リスト10 make testでテストを並列実行させる(Makele.PL)

use inc::Module::Install;
use Module::Install::TestTarget;
...
default_test_target(
    includes => ['t/lib'],
    run_on_prepare => ['t/script/setup_mysqld.pl'],
    env => { HARNESS_OPTIONS => 'j4' }, ←追加
);
...

しかし,このままだと1つのMySQLに対して複数のテストが同時に走ってしまうため,テストデータが混ざってしまい,正常なテストができなくなってしまいます。

そこで,テストの並列実行を抑止するためのモジュールTest::Synchronizedを,次のようにTest::MyApp::Fixture::DBIの冒頭でuseするようにします。

package Test::MyApp::Fixture::DBI;
use Test::Synchronized;
...

Test::Synchronizedはuseするだけでそのテストが並列実行されなくなりますので,結果としてTest::MyApp::Fixture::DBIを使用しているすべてのテストが並列実行されなくなります。当然,並列実行による高速化の恩恵は受けられなくなりますが,それでも個別にTest::mysqldを立ち上げるよりは多少速くテストが走る印象です。

このようにすると,Test::Synchronizedを使っているものは並列実行されなくなり,それ以外のテストはすべて並列で実行されるようになります。

また,Module::Install::TestTargetを使うと,maketestの実行段階でいろいろとフックすることができるので,より良いテスト方法を模索してみてはいかがでしょうか。

注14)
単にjとするとj9と同じになります。

おわりに

Mobage APIの実装を例に,どういったことに気をつけたら高速かつスケールしやすいプログラムを書けるかの一例を紹介しました。また,DBやmemcachedを使ったテストについても解説しました。より堅牢なアプリケーションを開発する手助けとなれば幸いです。

実際にはWeb APIを提供するうえで,OAuthなどの認証が必須となりますし,そもそもどういったAPIを提供するかが一番難しいところだったりします。もし今回の記事でWeb APIに少しでも興味が湧いたら,ご自身で実装してみると,いろいろと発見があっておもしろいかもしれません。

次回の執筆者は日ごろから筆者の隣に座って仕事をしている小林篤(nekokak)さんで,テーマは「ジョブキュー」です。お楽しみに!

著者プロフィール

嶋田裕二(しまだゆうじ)

1986年8月生まれ。Gaiaxなど数社を経て,現在はDeNAのプラットフォームシステムグループに所属。Mobagae APIやGadget Serverを担当し,日々増え続けるデータやアクセスに苦悩する日々。

CPANモジュールに「Windows対応用のパッチを送る迷惑な人」として一部で有名。

「Yokohama.pm」や「PerlCasual」,「YAPC::Asia」でスピーカをするなど,Perl関連のコミュニティへ積極的に参加している。

ブログ:http://blog.livedoor.jp/xaicron/

ハンドルネーム:xaicron