Perl Hackers Hub

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

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

自動テストのノウハウ

Web APIは入出力の形式がほぼ決まっているので,比較的,自動テストが書きやすいプログラムです。ここでは,特に重要なDBとmemcachedなどの外部装置を使ったテスト方法と,効率良くテストを走らせる方法について述べます。

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

Mobage APIではDBにMySQLを使用しています。少しでもパフォーマンスを稼ぐためにMySQLの独自SQLを使用している個所も少なくないため,正しい挙動をするかどうか,自動テストでも実際にMySQLに接続して処理を走らせています。

開発環境にアクセスして,テストを行ってもよいのですが,DBに更新がかかるような処理が走ると,正確なテストデータの整合性がとれなくなります。そのため,毎回クリーンなMySQLを立ち上げて,そこに固定のテストデータを入れてテストを行うようにしています。

テストが起動するたびにクリーンなMySQLを立ち上げるには,Test::mysqldを使用します。Test::mysqldを使用すると次のようにテスト用のMySQLを簡単に起動できます。

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

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

my $dbh = DBI->connect($mysqld->dsn(dbname => 'test'));

ここで起動したMySQLは$mysqldのスコープが抜けた段階で自動的に停止します。

当然,この状態ではDBやテーブルなどもなく,データも入っていません。そこでTest::Fixture::DBIを使用しMySQLに特定のデータをセットします。Test::Fixture::DBIを使うと,既存のDBからDB情報やデータを簡単に取得できます。DB情報の取得はmake_database_yaml.pl,テーブルのデータの取得はmake_database_yaml.plコマンドを使用します。

DBの情報はすべてのテストで共通で使えるため,t/schema配下にdb_name.yamlという名前で保存します。

$ make_database_yaml.pl \
    -d 'dbi:mysql:dbname=user;host=localhost' \
    -u root -o t/schema/user.yaml

また,テーブルのデータは,テスト対象のファイル名でディレクトリを作成し,その配下にtablename_fixture.yamlという名前で保存するようにしています。先ほどのt/model/user/get_friends.tというファイルに対するデータであればt/user/get_friends/friend_data_fixture.yamlなどです。以下は,userデータベースのfriendテーブルをYAMLファイルに落とし込む例です。

$ make_fixture_yaml.pl \
    -d 'dbi:mysql:dbname=user;host=localhost' \
    -u root -t friend_data -n user_id -n friend_id \
    -o t/user/get_friends/friend_data_fixture.yaml

詳しいオプションを知りたい方はそれぞれのコマンドで-hを付けて実行してください。

このように作成したデータを実際のテストコードでMySQLに読み込むために,リスト4のようなモジュールを書くと便利です。テスト専用のモジュールは慣例に従ってt/lib配下に置くとよいでしょう。これを使うと,リスト5のようにMySQLの立ち上げからデータの登録までを簡単に行えます。

リスト4テスト用のMySQLをセットアップするモジュール(t/lib/Test/MyApp/Fixture/DBI.pm)

package Test::MyApp::Fixture::DBI;
use strict;
use warnings;
use DBI;
use Test::mysqld;
use Test::Fixture::DBI
    qw(construct_database construct_fixture);
use DBIx::DBHResolver;
use Exporter 'import';

our @EXPORT = qw(start_mysql setup_database dbh);

sub start_mysql {
    my %config = @_;
    return Test::mysqld->new(my_cnf => +{
        'skip-networking' => '', %config,
    });
}

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

sub setup_database {
    my ($dbname, $fixtures) = @_;
    my $db_yaml = "t/schema/$dbname.yaml";

    my $dbh = dbh('mysql');
    $dbh->do("CREATE DATABASE IF NOT EXISTS $dbname");
    $dbh->do("USE $dbname");

    # データベースをsetup
    construct_database(
        dbh => $dbh,
        database => $db_yaml,
    );

    $dbh->{AutoCommit} = 0;

    # テーブルのデータを入れる
    for my $fixture (@$fixtures) {
        construct_fixture(
            dbh => $dbh,
            fixture => "$fixture",
        );
    }
}

1;

リスト5 Test::MyApp::Fixture::DBIの使用例(t/user/get_friends.t)

use lib 't/lib';
use Test::More;
use Path::Class qw/dir/;
use Test::MyApp::Fixture::DBI qw(
    start_mysql setup_database dbh
);
use MyApp::Model::User;
use MyApp::DB;

# $test_dirにはt/user/get_friendsが入る
my $test_dir = dir(__FILE__)->subdir('get_friends');

# MySQLを起動する
my $mysqld = start_mysql();

# fixtureを適用する
setup_database('friend', [
    $test_dir->file('friend_data_fixture.yaml'),
]);

# データベースハンドルを登録する
MyApp::DB->register(
    FRIEND_MASTER => dbh('friend', {AutoCommit => 0}),
    FRIEND_SLAVE => dbh('friend', {AutoCommit => 1}),
);

my $model = MyApp::Model::User->new;

sub test_get_friends {
    my %specs = @_;
    my ($input, $expects, $desc) = @_;

    subtest $desc => sub {
        # get_friendsは内部でfriend.friend_data
        # を参照している
        my $got = $model->get_friends($input);
        is_deeply $got, $expects;
    };
}

test_get_friends(...);
...
done_testing;

このように,Test::mysqldとTest::Fixture::DBIを利用すれば,常に固定のデータで自動テストを行うことができるようになります。だたし,スキーマの変更があった場合はすべてのデータを作りなおす必要があるため,変更に弱いテスト手法であるということは覚えておいたほうがよいでしょう。

著者プロフィール

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

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

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

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

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

ハンドルネーム:xaicron