Elasticsearchを使った開発をしていると、Elasticsearchを使った機能でも簡単にテストしたいという気持ちになってくる。またproveの-jオプションを使って並列に動かしていても変にコンフリクトせずにいい感じになってほしい。
この課題を解決するために、Harrietを用いてテスト実行前にElasticsearchを起動し、テストを行うというのをやってみた。
作戦
- Harrietを用いて、テスト実行前にElasticsearchのnodeを一つ起動する
- 並列数分、Elasticsearchのインデックスを作成し、ついでにロック用のインデックスも作っておく
- 環境変数で情報をテストファイルに渡す
- テストファイルでは環境変数から情報を読み込んで、ロックを取りつつ、立ち上がったportやインデックスにアクセスする
HarrietでElasticsearchの起動 & インデックスの作成
Harrietについてはtokuhirom blog を参照。
t/harriet/elasticsearch.plを用意して、以下のように書く。
use strict;
use warnings;
use utf8;
use JSON::XS qw(encode_json);
use Test::TCP;
use App::Prove;
use Search::Elasticsearch;
sub install_signal_handler ($) {
my ($sig) = @_;
my $original_handler = $SIG{$sig};
$SIG{$sig} = sub {
$HARRIET_GUARDS::ELASTICSEARCH = undef;
$SIG{$sig} = $original_handler || 'DEFAULT';
kill $sig => $$;
};
}
my $now = time;
my $test_process_pid = $$;
$HARRIET_GUARDS::ELASTICSEARCH = Test::TCP->new(
code => sub {
my $port = shift;
my $tcp_port = $port + 100;
exec(
'elasticsearch/bin/elasticsearch',
sprintf("--cluster.name=sample-test-%s-%s", $now, $test_process_pid),
"--http.port=$port",
"--transport.tcp.port=$tcp_port",
);
},
);
my $http_port = $HARRIET_GUARDS::ELASTICSEARCH->port;
my $es = Search::Elasticsearch->new(nodes => ["localhost:$http_port"]);
my $elasticsearch_index_names = [];
my $setup = sub {
my $j = shift;
my $index_name = "sample-$j";
$es->indices->create(
index => $index_name,
body => {
mappings => {
hoge => {
properties => {
foo => { type => 'string' },
},
},
},
},
);
$es->indices->put_mapping(
index => $index_name,
type => 'lock',
body => {
lock => {},
},
);
push @$elasticsearch_index_names, $index_name;
};
my $prove = App::Prove->new;
$prove->process_args(@ARGV);
my $jobs_n = $prove->jobs || 1;
$setup->($_) for (1..$jobs_n);
install_signal_handler($_) for qw(INT TERM);
$ENV{TEST_HARRIET_ELASTICSEARCH_JSON} = encode_json({
http_port => $http_port,
index_names => $elasticsearch_index_names,
});
すごく長いし難しいのだが、それぞれやっていることはコメントに書いた。ポイントとしては
- 並列に実行するとして、その台数分Elasticsearchを起動するのは重いので、インデックスを複数作るという方向で考える
- Elasticsearchはクラスタを勝手に作るので、クラスタ名が被らないように工夫する
- $HARRIET_GUARDSというところに入れておけば、テスト終了時にオブジェクトが破棄され、Elasticsearchを終了できる
テストファイルから起動したElasticsearch及び作成したインデックスにアクセスする
環境変数で情報は渡ってきているので、それをテストファイルから利用したら良い。
例えば以下のようにt/elasticsearch.tを作る。
use strict;
use warnings;
use utf8;
use lib 't/lib';
use Test::More;
use Search::Elasticsearch;
use JSON::XS qw(decode_json);
use Guard qw(guard);
my $http_port;
my $free_index_name;
my $elasticsearch_unlock;
if (my $elasticsearch = eval { decode_json($ENV{TEST_HARRIET_ELASTICSEARCH_JSON}) }) {
$http_port = $elasticsearch->{http_port};
my $LOCK_ID = 1;
my $es = Search::Elasticsearch->new(nodes => ["localhost:$http_port"]);
GET_INDEX: for my $count (1..100) {
for my $index_name (@{ $elasticsearch->{index_names} }) {
eval {
$es->create(
index => $index_name,
type => 'lock',
id => $LOCK_ID,
body => {},
);
};
unless ($@) {
$free_index_name = $index_name;
last GET_INDEX;
}
}
sleep(1);
}
die 'No elasticsearch index acquired' unless $free_index_name;
$elasticsearch_unlock = guard {
my $es = Search::Elasticsearch->new(
nodes => ["localhost:$http_port"],
);
$es->delete(
index => $free_index_name,
type => 'lock',
id => $LOCK_ID,
);
};
}
my $es = Search::Elasticsearch->new(nodes => ["localhost:$http_port"]);
ok $es->indices->exists(index => $free_index_name), 'index exists';
undef $elasticsearch_unlock;
done_testing();
これはベタッと書いてみたけど、あとはこのロックを取る部分をメソッド化してあげて、返り値でguardオブジェクトやportや取得したインデックス名を受け取れば、いろんなテストで利用できるはず。
まとめ
今回はElasticsearchを使っているPerlプロジェクトでテストをいい感じにする方法について書いた。結構泥臭い感じになっているので、もっといい感じの方法があったら嬉しい。