embedded-elasticsearchで手軽にElasticsearchのJUnitテスト 2018年版 

# Embedded Elasticsearch: テストコード内でESを起動

皆さんはElasticsearchを使用したアプリケーションの単体テスト・結合テストはどのように行っていますか?
ESのクライアントのモックを作成して注入する、外部で起動しているESインスタンスに接続する、など色々な方法があると思いますが、Javaアプリケーションの開発に限ってはEmbedded Elasticsearchを用いた便利な方法がかつて存在しました。
つまりES自体がJavaアプリケーションなので、テストを実行するJVM上でESの``Node``も一緒に起動してテスト用のESインスタンスとして使う方法です。テスト用とはいえモックではない本物のESインスタンスですのでインデキシングも検索も普通に動作する大変便利なものでした。またelastic自身も``ESIntegTestCase``というJUnitテストのひな形を提供しており、私も[数年前に記事を書きました](https://qiita.com/sugibuchi/items/cf8850baaa6516cd5455)。

ところが、バージョン5以降、このEmbedded Elasticsearchはどんどん使いにくくなる状況にあります。

* elastic自身が[Embedded Elasticsearchはサポート外](https://www.elastic.co/blog/elasticsearch-the-server)と宣言
* 公式PluginがMaven Centralにアップロードされなくなった
* Elasticsearch本体もモジュール化が進んだが、重要なモジュールがやはりアップロードされなくなった

会社でもEmbedded Elasticsearchを利用したテストフレームワークを内製してガッツリ活用していたため、ES2.xからアップグレードする際には同僚とうぉ〜とぼやきながらテストに必要なモジュールを社内のMavenレポジトリに一つづつ登録するという煩雑な作業が必要になりました。

似たような問題を抱えた方は他にもいたようで、elasticのフォーラムなどを調べてると以下のような代替策が提案されていました。

* [Elasticsearch Maven Plugin](https://github.com/alexcojocaru/elasticsearch-maven-plugin)を使う
* Docker等でテスト用のESインスタンスを実行する
* Gradleを用いる方法もあるらしい

要するに、ビルドツールと連携してテスト用のESインスタンスを実行するという方法です。
これはこれで使えるのですが、ビルドツールを介さずともIDE上で直接テストケースを実行できる手軽さ、ESノードの設定や起動も直接テストコード中からプログラマブルに制御できる使い勝手の良さなど、Embedded Elasticsearchの利点の多くが失われています。

というわけで、ES5.x以降でもEmbedded Elasticsearchっぽいことを実現するライブラリがいくつか開発されています。

* [``codelibs/elasticsearch-cluster-runner``](https://github.com/codelibs/elasticsearch-cluster-runner)
* [``allegro/embedded-elasticsearch``](https://github.com/allegro/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上での問題が修正されたので最新版をお薦めします。

```xml:pom.xml
  <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とも問題無く使えます。

## テストコード

テストコードはこのような形になります。

```java
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(...)``メソッドで指定出来ます

```java
  ...
  @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``を使う記事も書いてみようかと思います。