Embedded Elasticsearch: テストコード内でESを起動
皆さんはElasticsearchを使用したアプリケーションの単体テスト・結合テストはどのように行っていますか?
ESのクライアントのモックを作成して注入する、外部で起動しているESインスタンスに接続する、など色々な方法があると思いますが、Javaアプリケーションの開発に限ってはEmbedded Elasticsearchを用いた便利な方法がかつて存在しました。
つまりES自体がJavaアプリケーションなので、テストを実行するJVM上でESのNode
も一緒に起動してテスト用のESインスタンスとして使う方法です。テスト用とはいえモックではない本物のESインスタンスですのでインデキシングも検索も普通に動作する大変便利なものでした。またelastic自身もESIntegTestCase
というJUnitテストのひな形を提供しており、私も数年前に記事を書きました。
ところが、バージョン5以降、このEmbedded Elasticsearchはどんどん使いにくくなる状況にあります。
- elastic自身がEmbedded Elasticsearchはサポート外と宣言
- 公式PluginがMaven Centralにアップロードされなくなった
- Elasticsearch本体もモジュール化が進んだが、重要なモジュールがやはりアップロードされなくなった
会社でもEmbedded Elasticsearchを利用したテストフレームワークを内製してガッツリ活用していたため、ES2.xからアップグレードする際には同僚とうぉ〜とぼやきながらテストに必要なモジュールを社内のMavenレポジトリに一つづつ登録するという煩雑な作業が必要になりました。
似たような問題を抱えた方は他にもいたようで、elasticのフォーラムなどを調べてると以下のような代替策が提案されていました。
- Elasticsearch Maven Pluginを使う
- Docker等でテスト用のESインスタンスを実行する
- Gradleを用いる方法もあるらしい
要するに、ビルドツールと連携してテスト用のESインスタンスを実行するという方法です。
これはこれで使えるのですが、ビルドツールを介さずともIDE上で直接テストケースを実行できる手軽さ、ESノードの設定や起動も直接テストコード中からプログラマブルに制御できる使い勝手の良さなど、Embedded Elasticsearchの利点の多くが失われています。
というわけで、ES5.x以降でもEmbedded Elasticsearchっぽいことを実現するライブラリがいくつか開発されています。
elasticsearch-cluster-runner
はES5.x以降でもEmbedded Elasticsearchを使用するためのライブラリです。Maven CentralにアップロードされていないESのモジュールは自前でMaven Centralにアップロードされています。頭が下がります。
embedded-elasticsearch
は少し異なるアプローチです。embedded-elasticsearch
という名前とは裏腹に、このライブラリはESをテストと同じJVM上で実行しません。代わりにテストの度にESインスタンスを以下の手順で外部プロセスとして実行します。
- 公式リポジトリからElasticsearchの配布パッケージをダウンロードする
- ダウンロードしたパッケージを展開する
- 設定ファイルを作成する
- プラグインをインストールする
-
bin/elasticsearch
を実行してESインスタンスを起動する - テスト終了後ESインスタンスのプロセスを停止する
つまり、普通にESをダウンロードして実行するのと同じ環境が得られます。他方でESノードの制御がテストコード中からプログラマブルに行える使い勝手の良さはEmbedded Elasticsearchと同様です。
それぞれ一長一短があります
-
elasticsearch-cluster-runner
: 起動が早い・より高機能・デフォルトで複数ノード立ち上げ -
embedded-elasticsearch
: 依存性がとても少ない
Embedded Elasticsearchは同じJVM上でESも起動する必要上、ESの実行に必要な依存性が全てクラスパスに追加されます。そのため本来テストしたいアプリケーションの依存性と衝突してしまうこともあるかもしれません。他方でembedded-elasticsearch
はESは外部プロセスとして実行されるため同様の問題は起きません。embedded-elasticsearch
自身もJacksonやいくつかのcommonsライブラリのみに依存する大変軽量なものとなっています。
前口上が長くなりましたが、この記事ではembedded-elasticsearch
をJUnit上から使ってみます。
embedded-elasticsearch
の基本的な使い方
依存性の設定
pl.allegro.tech:embedded-elasticsearch
を依存性に追加します。最新版は2.5.0
です。最近Windows上での問題が修正されたので最新版をお薦めします。
<dependencies>
...
<dependency>
<groupId>pl.allegro.tech</groupId>
<artifactId>embedded-elasticsearch</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
embedded-elasticsearch
はロギングにSLF4Jを使用しますが依存性としてslf4j-api
しか含まないため依存性に適当なbindingが存在しない場合はこれも追加します(上記の例ではslf4j-simple
)。
今回の例ではテストフレームワークとしてJUnit5を使用していますが、もちろんJUnit4やScalaTestとも問題無く使えます。
テストコード
テストコードはこのような形になります。
public class TestEmbeddedES {
private static EmbeddedElastic es = null;
@BeforeAll
public static void startES() throws Exception {
es = EmbeddedElastic.builder()
.withElasticVersion("6.2.4")
.withSetting("discovery.zen.ping.unicast.hosts", Collections.emptyList()) // <- (*)
.build();
es.start();
}
@Test
void check_es_status() throws IOException {
System.out.println("HTTP port: " + es.getHttpPort());
System.out.println("Transport port: " + es.getTransportTcpPort());
CloseableHttpClient client = HttpClientBuilder.create().build();
String uri = "http://localhost:" + es.getHttpPort() + "/";
CloseableHttpResponse res = client.execute(new HttpGet(uri));
System.out.println(IOUtils.toString(res.getEntity().getContent(), "UTF-8"));
}
@AfterAll
public static void stopES() {
es.stop();
}
}
全てのテストの実行前(@BeforeAll
)にEmbeddedElastic
のインスタンスをビルダーを用いて作成します。ビルダーを通してESのバージョン指定や設定(最終的にconfig/elasticsearch.yml
に書き込まれる)を行う事が出来ます。(*)で示された設定はESのZenディスカバリを無効化する回避策です。同じマシン上でESが実行中の場合、この設定が無いとテスト用に起動したESノードが誤って接続してしまう恐れがあります。
EmbeddedElastic#start()
を呼ぶことでESが起動します。起動したESに接続するためのポート番号はgetHttpPort()
やgetTransportTcpPort()
を呼ぶことで取得出来ます。実際のテストではこれらのポート番号を用いてクライアントを作成したりテストを行うサービスを設定したりすることになるかと思います。
全てのテストの終了後(@AfterAll
)に忘れずにEmbeddedElastic#stop()
を呼んでESを停止する必要があります。
基本的な動作
上記のテストを実行すると以下のようなログが出力されます。
[main] INFO p.a.t.e.ElasticSearchInstaller - Downloading https://artifacts.elastic.co/.../elasticsearch-6.2.4.zip to /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Download complete
[main] INFO p.a.t.e.ElasticSearchInstaller - Installing Elasticsearch into /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Done
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch.in.sh
[main] INFO p.a.t.e.ElasticServer - Waiting for ElasticSearch to start...
[EmbeddedElsHandler] INFO p.a.t.e.ElasticServer - [2018-04-20T00:40:05,110][INFO ][o.e.n.Node ] [] initializing ...
...
ログにも表示されているように、embedded-elasticsearch
はまず指定されたバージョンのESの配布パッケージをelasticのサーバからダウンロードします。ダウンロードが完了したらそれを一時ディレクトリ内に解凍しファイルの実行属性の設定を行ってからESを実行します。
ダウンロードされたESの配布パッケージはテンポラリディレクトリ内にキャッシュされます。テンポラリディレクトリが掃除されていなければ二回目以降はダウンロードをスキップします。
[main] INFO p.a.t.e.ElasticSearchInstaller - Download skipped
[main] INFO p.a.t.e.ElasticSearchInstaller - Installing Elasticsearch into /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Done
...
しかし初期設定ではテストクラスの実行の度にESの配布パッケージの解凍を行うため、その分elasticsearch-cluster-runner
よりも遅いです。
プラグインの使用
ESインスタンスと共に使用するプラグインはEmbeddedElastic.Builder
のwithPlugin(...)
メソッドで指定出来ます
...
@BeforeAll
public static void startES() throws Exception {
es = EmbeddedElastic.builder()
.withElasticVersion("6.2.4")
.withPlugin("analysis-phonetic")
.withPlugin("analysis-icu")
.withSetting("discovery.zen.ping.unicast.hosts", Collections.emptyList())
.build();
...
プラグインが指定された場合、ESの起動前にbin/elasticserch-plugin
コマンドを用いてプラグインがインストールされます。
...
[main] INFO p.a.t.e.ElasticSearchInstaller - > /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin install analysis-phonetic
-> Downloading analysis-phonetic from elastic
[=================================================] 100%
-> Installed analysis-phonetic
[main] INFO p.a.t.e.ElasticSearchInstaller - > /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin install analysis-icu
-> Downloading analysis-icu from elastic
[=================================================] 100%
-> Installed analysis-icu
...
ここで問題になるのは、ESの配布パッケージは一旦ダウンロードするとそれがキャッシュされるのに対して、プラグインはテストの度に毎回ダウンロードされることです。phonetic analysisプラグインのように小さなプラグインならともかく、ICUプラグインのような大きなプラグインをテストの度にダウンロードするのはとても苦痛です。
残念ながらembedded-elasticsearch
ライブラリ自体は現状特にこの問題に対する解決策を持っていません。ただwithPlugin(...)
メソッドも内部的にはbin/elasticserch-plugin
を実行しているためwithPlugin(...)
メソッドの引数にはプラグイン名のみならずURLやローカルファイルのPathも指定出来ます。
ですのでテストにプラグインを用いたい場合、現実的にはEmbeddedElastic
の起動前にプラグインのパッケージをダウンロードしておきローカルにキャッシュする仕組みを自前で実装する必要があるかと思います。
で、どちらのライブラリを使えば良いのか
アプリケーションで利用しているESクライアントの種類によります。
Transportクライアントは現状elasticsearch-core
他多数のESモジュールに依存しています。そのためtransport
モジュールを依存性に追加した時点でクラスパスの中身はelasticsearch-cluster-runner
を使った場合と大差なくなります。
ですのでこの場合は起動が速く高機能なelasticsearch-cluster-runner
が良い選択になるかと思います。
他方で依存性が少ないRESTクライアントを使っていて依存性をあまり増やしたくない場合はembedded-elasticsearch
も良い選択肢になりえます。
ただ今回の記事を書くに当たってelasticsearch-cluster-runner
を調べ直したのですが、ずっと高機能ですし、Elasticsearch自身も改めて確認するとそう困るような依存性の多さではないので、embedded-elasticsearch
を使うべきシーンは思ったより少ないかなぁと思い直しています。今回は使い慣れたembedded-elasticsearch
の記事ですが、elasticsearch-cluster-runner
を使う記事も書いてみようかと思います。