大量にあるサーバへのアクセスを効率的に扱う
Mobage APIでは、
DBへのアクセスを隠蔽する
DBは一般的な、
このように大量にあるDBへのアクセスを簡単に行うために、
use DBIx::DBHResolver;
my $resolver = DBIx::DBHResolver->new;
$resolver->load('db_config.yaml'); …①
my $dbh1 = $resolver->connect('MASTER002'); …②
my $dbh2 = $resolver->connect('MASTER', 1234);…③
my $dbh3 = $resolver->connect('SLAVE', 2345); …④
...
---
clusters:
MASTER:
- MASTER001
- MASTER002
SLAVE:
- SLAVE001
- SLAVE002
connect_info:
MASTER001:
attrs:
AutoCommit: 0
RaiseError: 1
dsn: dbi:mysql:dbname=test;host=master001;
password: ~
user: root
SLAVE001:
attrs:
AutoCommit: 1
RaiseError: 1
dsn: dbi:mysql:dbname=test;host=slave001;
password: ~
user: root
...
リスト2 ①の$resolver->load()
はConfig::Anyを使っているのでYAML
リスト2 ③はShardingの例です。リスト3のclustersの部分がShardingの設定になります。MASTERがこのクラスタの名前で、$resolver->connect()
の第2引数の値で剰余演算をし、
DBIx::DBHResolverを使うと、 memcachedも用途によって複数のサーバ/ Web APIは入出力の形式がほぼ決まっているので、 Mobage APIではDBにMySQLを使用しています。少しでもパフォーマンスを稼ぐためにMySQLの独自SQLを使用している個所も少なくないため、 開発環境にアクセスして、 テストが起動するたびにクリーンなMySQLを立ち上げるには、 ここで起動したMySQLは$mysqldのスコープが抜けた段階で自動的に停止します。 当然、 DBの情報はすべてのテストで共通で使えるため、 また、 詳しいオプションを知りたい方はそれぞれのコマンドで-hを付けて実行してください。 このように作成したデータを実際のテストコードでMySQLに読み込むために、 このように、 DBと同様にmemcachedもテストのたびに立ち上げて、 memcachedをテストごとに立ち上げるためにはProc::GuardとTest::TCPを使用します。リスト6のようにmemcachedを立ち上げるモジュールを書いておくと便利でしょう。これを使うと、 このようにテストごとに新しいmemcachedを立ち上げることで、 テストが増えると、 Test::mysqldを使ったテストの場合、 というようにしています。まず、 ①の$ENV{TEST_ make testのときにMySQLを立ち上げるには、 これで、 DBのテストは速くなりましたが、 この設定を有効にするには、 しかし、 そこで、 Test::Synchronizedはuseするだけでそのテストが並列実行されなくなりますので、 このようにすると、 また、 Mobage APIの実装を例に、 実際にはWeb APIを提供するうえで、 次回の執筆者は日ごろから筆者の隣に座って仕事をしている小林篤memcachedへのアクセスを隠蔽する
自動テストのノウハウ
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'));
$ make_database_yaml.pl \
-d 'dbi:mysql:dbname=user;host=localhost' \
-u root -o t/schema/user.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
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;
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;
memcachedを使ったテストの書き方
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;
}
make testを高速化!
MySQLをあらかじめ立ち上げておく
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");
}
}
...
use inc::Module::Install;
use Module::Install::TestTarget;
...
default_test_target(
includes => ['t/lib'],
run_on_prepare => ['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 };
できるだけテストを並列で実行する
j数字
という文字列を入れると、j4
とすれば4つのテストが並列に実行されるので、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' }, ←追加
);
...
package Test::MyApp::Fixture::DBI;
use Test::Synchronized;
...
おわりに