ESIntegTestCaseで手軽にElasticsearchの結合テスト


Elasticsearchは標準で``ESIntegTestCase``という結合テストを手軽に行うためのクラスを提供しています。このクラスはテストの実行毎にテスト専用のElasticsearchの立ち上げ・破棄を自動的に行います。Elasticsearchのインストールも不要でプロジェクトに依存性を追加するだけで使えるため、Elasticsearchに実際にアクセスして行う結合テストを簡単に書くことができます。

Elasticsearchバージョン1.xでは元々``ElasticsearchIntegrationTest``と呼ばれていたクラスでした。バージョン2.xになって名前が変わり、また機能も追加されたようです。

* 参考: [ElasticsearchIntegrationTestを使ってElasticsearchのJUnitテストを書く](http://qiita.com/akima/items/981b336e5587e0d82268)

このクラスは非常に便利です! ただしドキュメントが少なくその振る舞いにも若干落とし穴があります。実際に使ってみて引っかかったポイントを中心に簡単にまとめます。

* 参考: [公式ドキュメント: Integration tests](https://www.elastic.co/guide/en/elasticsearch/reference/current/integration-tests.html)

# 導入

```xml:pom.xml
  <dependencies>
    ...
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>2.3.4</version>
    </dependency>
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>2.3.4</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-test-framework</artifactId>
      <version>5.5.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.3</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna</artifactId>
      <version>4.1.0</version>
      <scope>test</scope>
    </dependency>
    ...
  </dependencies>
```

``org.elasticsearch:elasticsearch:jar``とは別に``org.elasticsearch:elasticsearch:test-jar``を依存性に追加する必要があります。
またこれ以外に以下の依存性も別個に追加する必要があります。

* lucene-test-framework (公式ドキュメントに記載あり)
* hamcrest-library
* jna

各ライブラリのバージョンはElasticsearchのParent pom.xmlを確認して下さい。上記の例中のバージョンはElasticsearch 2.3.4のものです。

# 使い方

``ESIntegTestCase``を継承したテストクラスを作成するだけです。


```java:SampleTest.java
// ESIntegTestCaseクラスを継承したテストクラスを作成
public class SampleTest extends ESIntegTestCase {

  @Test
  public void test() throws Exception {

    // client()メソッドでクライアントを取得 -> インデックスを作成
    client().admin().indices().prepareCreate("pizza").get();

    for (String[] p : PIZZA_ITALIANA) {
      // 文章をインデックス
      client().prepareIndex("pizza", "recipe").setSource("name", p[0], "topping", p[1]).get();
    }

    // クライアントを使わず直接インデックスを操作するメソッドも用意されている
    flushAndRefresh("pizza");

    // インデックスされたレシピからキノコ(funghi)を含むピザを検索
    SearchResponse res = client().prepareSearch("pizza").setQuery(queryStringQuery("funghi")).get();

    // ヒット数を確認
    assertEquals(5, res.getHits().totalHits());
  }

  private static final String[][] PIZZA_ITALIANA = {
    {"Margherita", "pomodoro, fiordilatte, basilico, olio"},
    {"Marinara", "pomodoro, aglio, origano, olio"},
    {"Napoletana", "pomodoro, acciughe salate, origano, capperi, olio"},
    {"Capricciosa ", "pomodoro, mozzarella, funghi, carciofini, prosciutto cotto o crudo, olive nere"},
    {"Quattro formaggi", "pomodoro, mozzarella, altri formaggi a discrezione, basilico"},
    {"Funghi", "pomodoro, mozzarella, funghi champignon, prezzemolo, olio"},
    {"Prosciutto e funghi", "pomodoro, mozzarella, funghi champignon, prosciutto cotto o crudo"},
    {"Diavola", "pomodoro, mozzarella, salame piccante, peperoncino, olive nere"},
    {"Boscaiola", "mozzarella, funghi champignon, salsiccia"},
    {"Fiori di zucca", "mozzarella, fiori di zucca, acciughe salate, olio"},
    {"Frutti di mare", "frutti di mare, aglio, prezzemolo"},
    {"Ortolana", "pomodoro, mozzarella, peperoni, melanzane, zucchine, altre verdure grigliate a scelta"},
    {"Tirolese", "pomodoro, mozzarella, gorgonzola, speck"},
    {"Bismarck", "pomodoro, mozzarella, prosciutto cotto, uovo fritto"},
    {"Mare e monti", "pomodoro, mozzarella, funghi, gamberetti, cozze, calamari, aglio, prezzemolo"},
    {"Carrettiera", "mozzarella, friarielli, salsiccia, scaglie di grana"},
    {"Mimosa", "mozzarella, panna, prosciutto cotto, mais"},
    {"Primavera", "pomodori pachino, mozzarella, rucola, prosciutto crudo, scaglie di grana"},
    {"Caprese", "pomodori pachino, bufala, basilico, olio"}
  };
}
```

**注意**: 環境によってはテストが``found jar hell in test classpath``というエラーと共に停止する場合があります。この場合は次の章のトラブルシューティングをまず参照して下さい。

テストケース中では継承した``ESIntegTestCase``クラスの``client()``メソッドを用いてElasticsearchクライアントを取得することが出来ます。

また``createIndex(...)``や``flushAndRefresh(...)``といったクライアントを経由せず直接Elasticsearchを操作する便利メソッドも用意されています。

* 参考: [Generic helper methods](https://www.elastic.co/guide/en/elasticsearch/reference/current/integration-tests.html#helper-methods)

実際にテストを書く場合、便利メソッドはインデックスの作成等の下準備に用いて、``client()``メソッドで取得したクライアントを検証するコンポーネントに依存性として注入してテストを実行するかたちになるかと思います。

デフォルトではテストクラスの実行前にElasticsearchクラスタが自動起動し、実行後に停止されます(``@BeforeClass``と``@AfterClass``)。これをテストケース毎にクラスタの立ち上げ・停止を行うには次のアノテーションをテストクラスに付けます(当然テストの実行にはより時間がかかるようになります)。

```java:SampleTest.java
@ESIntegTestCase.ClusterScope(scope= ESIntegTestCase.Scope.TEST)
public class SampleTest extends ESIntegTestCase {
```

# トラブルシューティング

``ESIntegTestCase``を用いたテストではローカルファイルのアクセスやリフレクションを用いたライブラリ(MockitoやJackson-databindなど)がアクセス拒否で動作しない事があります。また、``found jar hell in test classpath``というエラーと共にテストの実行が停止してしまうことがあります。

これは元々``ESIntegTestCase``がElasticsearchそのものやプラグインの開発用途を想定しているためか、テストの実行前にセキュリティマネージャの導入や依存性衝突の厳密なチェックを強制的に行うためです。

* 参考: [BootstrapForTesting.java (Github)](https://github.com/elastic/elasticsearch/blob/2.3/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java#L71)

この前処理は``ESIntegTestCase``のクラスロード時に実行されるため、テストクラスのコード上からこれをカスタマイズする事は出来ません。代わりに以下のシステムプロパティを設定することでこれらの前処理を無効化できます

* ``tests.security.manager``: セキュリティマネージャの有効化・無効化
* ``tests.jarhell.check``: 依存性衝突のチェックの有効化・無効化

(注: バージョン5.0のコード中にはこの``tests.jarhell.check``が見当たらないのが気になるところです・・・)

これらのプロパティを設定するには``java``コマンドにコマンドラインオプションを設定するか、

```
java -Dtests.security.manager=false -Dtests.jarhell.check=false ...
```

Maven等のビルドツール設定にテスト実行時のシステムプロパティを追加する必要があります。

```xml:pom.xml
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.19.1</version>
        <configuration>
          <systemPropertyVariables>
             <!-- セキュリティマネージャと依存性衝突のチェックを無効化 -->
            <tests.security.manager>false</tests.security.manager>
            <tests.jarhell.check>false</tests.jarhell.check>
          </systemPropertyVariables>
        </configuration>
      </plugin>
      ...
    </plugins>
  </build>
```