ElasticsearchIntegrationTestを使ってElasticsearchのJUnitテストを書く

# バージョン情報
* Elasticsearch 1.7.4

本記事が扱う`ElasticsearchIntegrationTest`はバージョン2系から`ESIntegTestCase`へと名前が変わっているようです. 伴って大きな変更が入っているかもしれませんので, ご注意ください.

# はじめに
## 概要
この記事は[Elasticsearch Advent Calendar 2015](http://qiita.com/advent-calendar/2015/elasticsearch)の3日目のエントリです。

実はElasticsearchはJavaでテストを書くための, `ElasticsearchIntegrationTest`というクラスを提供しています. が, 日本語の資料がめっちゃ少ない！（というか無い？）ので, 私が導入までに調べたこととかをまとめてみました.

これを使うと, テストを実行したマシン上で簡易Elasticsearchクラスタが立ち上がり, そのクラスタに対してテストケース内からドキュメントを投入したり, 検索したりできるようになります. テスト用にわざわざElasticsearchサーバーを立ち上げる必要がなくなりますよ！

## 公式ドキュメント (Elasticsearch 1.7)
Elasticsearch公式で触れているのはこのあたりの記事になります.

* [Java Testing Framework](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/testing-framework.html)
* [Using the elasticsearch test classes](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/using-elasticsearch-test-classes.html)
* [integration tests](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/integration-tests.html)

# テストに必要な依存関係
本記事ではMavenを使います. とりあえず`elasticsearch 1.7.4`で`ElasticsearchIntegrationTest`を試してみるための依存の記述は次のようになります.

```xml:pom.xml
<dependencies>
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-test-framework</artifactId>
        <version>4.10.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.carrotsearch.randomizedtesting</groupId>
        <artifactId>randomizedtesting-runner</artifactId>
        <version>2.1.17</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>1.7.3</version>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>1.7.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
  </dependencies>
```

記述順序が変わると動かなくなったりするので注意してください.

バージョンの決め方としては, まず`elasticsearch`のバージョンに自分が使っているものを指定します. `<type>test-jar</type>`の方も含めると2箇所です. 今回は`1.7.3`を指定しました.

次に, 使っているElasticsearchが依存しているLuceneのバージョンを調べ, `lucene-test-framework`のバージョンを決めます. EclipseのMaven POM エディターで依存関係を調べたり, [Maven Repositoryの当該バージョンのページ](http://mvnrepository.com/artifact/org.elasticsearch/elasticsearch/1.7.3)を見たりするのがいいんじゃないかと思います. 今回は`4.10.4`を使えばよいことが分かります.

最後に, `randomizedtesting-runner`のバージョンを決定するのですが, これは調べ方が分かりませんでした.
`lucene-test-framework 4.10.4`が依存している`randomizedtesting-runner 2.1.6`を使えばよいのかと思いきや, なんか動きませんでした. `2.1`系の最新版である`2.1.17`を指定したら動きました. 本当は `lucene-test-framework` で `randomizedtesting-runner` をExcludeした方が良いのかもしれない.

# テスト対象コード
以下のような, Elasticsearchクライアントをコンストラクタで受け取り, `search`メソッドでごく簡単な検索を行うクラスをテスト対象とします.

```java:src/main/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearch.java
package jp.akimateras.elasticsearch.esintegtest.service;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.index.query.MatchQueryBuilder.Operator;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;

public class SimpleSearch {
    // インデックス・タイプ名定義.
    public static final String INDEX_NAME = "simple-search";
    public static final String TYPE_NAME = "documents";

    // フィールド名定義.
    public static final String TITLE_FIELD_NAME = "title";
    public static final String TEXT_FIELD_NAME = "text";

    // 検索結果の返却型.
    public static class Document {
        public final String title;
        public final String text;

        Document(String title, String text) {
            this.title = title;
            this.text = text;
        }
    }

    // Elasticsearchクライアント.
    private final Client client;

    /**
     * コンストラクタ.
     * 
     * @param client 検索に使用するElasticsearchクライアント
     */
    public SimpleSearch(Client client) {
        this.client = client;
    }

    /**
     * 検索メソッド
     * 
     * @param keywords 検索キーワード. スペース区切りで複数のキーワードを指定した場合はAND検索とする.
     * @return 検索結果.
     */
    public Collection<Document> search(String keywords) throws IOException {
        try {
            // 検索クエリの組み立て.
            MultiMatchQueryBuilder query = QueryBuilders
                .multiMatchQuery(keywords, TITLE_FIELD_NAME, TEXT_FIELD_NAME)
                .operator(Operator.AND);

            // 検索の実行.
            SearchResponse response = client.prepareSearch(INDEX_NAME)
                .setTypes(TYPE_NAME)
                .setQuery(query)
                .execute()
                .get();

            // 検索結果を Collection<Document> に詰め替えてreturnする.
            return Arrays.stream(response.getHits().hits())
                .map(SearchHit::sourceAsMap)
                .map(source -> new Document(
                    (String) source.get(TITLE_FIELD_NAME),
                    (String) source.get(TEXT_FIELD_NAME)
                    ))
                .collect(Collectors.toList());
        } catch(Exception e) {
            throw new IOException(e);
        }
    }
}
```

# シンプルなテスト
上で作った`SimpleSearch`クラスを雑にテストしてみましょう.

`ElasticsearchIntegrationTest`でテストを書くためには, 

1. テストクラスに`ElasticsearchIntegrationTest`を継承させる.
2. テストメソッド内で`client()`を呼び出すとElasticsearchクライアントが取れる.

の2点だけ押さえておけばOKです.

```java:src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
package jp.akimateras.elasticsearch.esintegtest.service;

import static org.hamcrest.Matchers.is;

import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Before;
import org.junit.Test;

@ElasticsearchIntegrationTest.ClusterScope(numDataNodes = 1 /* テスト時に立ち上げるノード数 */)
public class SimpleSearchTest extends ElasticsearchIntegrationTest {
    // テスト用ノードに設定したい内容はこのメソッドの返却値として表現する.
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .build();
    }

	// ここでインデックスを作成する.
    @Before
    public void initializeIndex() throws Exception {
        // マッピング定義.
        // 外部JSONファイルとかでマッピング定義をしているなら, そっちから読んだ方が良いと思う.
        XContentBuilder mapping = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(SimpleSearch.TYPE_NAME)
                    .startObject("properties")
                        .startObject(SimpleSearch.TITLE_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                        .endObject()
                        .startObject(SimpleSearch.TEXT_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();

        // インデックス作成.
        admin().indices() // admin()でアドミンクライアントが取れる！
            .prepareCreate(SimpleSearch.INDEX_NAME)
            .addMapping(SimpleSearch.TYPE_NAME, mapping)
            .execute()
            .actionGet();

        // インデックス作成完了の待機.
        ensureGreen();
    }

	// テスト本体
    @Test
    public void testSearch() throws Exception {
        // ドキュメント投入.
        // これも外部JSONファイルなどを用意して, Bulk APIで投入した方が良いと思う.
        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Alpaca")
                .field(SimpleSearch.TEXT_FIELD_NAME, "An alpaca is a domesticated species of South American camelid.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Llama")
                .field(SimpleSearch.TEXT_FIELD_NAME, "A llama is a domesticated species of South American camelid.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Vicugna")
                .field(SimpleSearch.TEXT_FIELD_NAME, "A vicugna is a kind of herbivorous mammals that inhabit the Andes of South America.")
            .endObject());

        // ドキュメント投入完了の待機.
        flushAndRefresh();
        ensureGreen();

        // テスト用クライアントに対してSimpleSearchインスタンスを生成.
        SimpleSearch service = new SimpleSearch(client()); // client()でクライアントが取れる！
        
        // テスト実施.
        // せっかくなので, 依存のおまけで付いてきたhamcrestを使おう.
        assertThat(service.search("alpaca").size(), is(1));
        assertThat(service.search("llama").size(), is(1));
        assertThat(service.search("vicugna").size(), is(1));
        assertThat(service.search("lama").size(), is(0));

        assertThat(service.search("domesticated").size(), is(2));
        assertThat(service.search("alpaca domesticated").size(), is(1));
        assertThat(service.search("llama domesticated").size(), is(1));
        assertThat(service.search("vicugna domesticated").size(), is(0));
    }
}
```

`client()`でElasticsearchクライアントが取れるので, 基本的にはもう何でもできますね. インデックス作成などにアドミンクライアントが必要な場合は`client().admin()`または, `admin()`で取得してください. 

`@Before`でインデックスを作成していますが, `@After`で自前でインデックスを削除する必要はなく, テストメソッド毎に自動的にまっさらにしてくれるみたいです. 便利.

`ensureGreen()`や`flushAndRefresh()`など, 普段見かけないメソッドが登場していますが, これらは`ElasticsearchIntegrationTest`が提供してくれる便利メソッドです. たとえば `ensureGreen()` を自前で書くとすると,

```java
client().admin().cluster().prepareHealth()
    .setWaitForGreenStatus()
    .execute()
    .actionGet();
```

みたいにちょっと長くなります. もちろん, この手の操作をテストするために, 自前で書いてもOKです. 便利メソッドの一覧は[公式: integration tests (1.7)](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/integration-tests.html#helper-methods)を参照.

# ここまでで動かないとき
私が踏んだエラーのログとその対処を書いておきます.

## 型 <クラス名> の階層は不整合です
```text:Eclipseエラー
型 <クラス名> の階層は不整合です
The hierarchy of the type '<Class Name>' is inconsistent
```
Elasticsearchのテストフレームワークのjarは見えており, かつ, `randomizedtesting-runner`のjarが見えていない時に出たエラーです. Eclipseで`ElasticsearchIntegrationTest`をextendsしたテストクラスに赤線が引かれ, 上記エラーメッセージが出ます. 依存が正しく書けているかの確認や, プロジェクトのリフレッシュ/クリーンなど試してみてください.

## Test class requires enabled assertions
```text:実行時出力
java.lang.Exception: Test class requires enabled assertions, enable globally (-ea) or for Solr/Lucene subpackages only: jp.akimateras.elasticsearch.esintegtest.test.SampleTest
    __randomizedtesting.SeedInfo.seed([E05E1B5BAB0E8E6]:0)
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:38)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)
```

`ElasticsearchIntegrationTest`を使う場合, テスト実行時にアサーションが有効になっていないと駄目なようです. JVMの起動オプションに`-ea`を付けてあげてください.
Eclipseからテストを実行する場合は, `[実行の構成]`で対象の構成オプションを開き, `[引数]`タブの`[VM 引数]`テキストエリアに`-ea`と書いてあげればOKです.

## tests-framework.jarとlucene-core.jarがどうのこうの
```text:実行時出力
java.lang.AssertionError: fix your classpath to have tests-framework.jar before lucene-core.jar
    __randomizedtesting.SeedInfo.seed([C7B320FCEA3DEB94]:0)
    org.apache.lucene.util.TestRuleSetupAndRestoreClassEnv.before(TestRuleSetupAndRestoreClassEnv.java:211)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:45)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)
```

`pom.xml`の記述順序が間違っています. Javaのクラスパスの順序が問題になるのかな...？
`randomizedtesting-runner`への依存の前にElasticsearchへの依存を書くと発生するようです. Javaに詳しくないので理由はよく分かりませんが, 記述順序で動かないことがあるのはちょっと嫌な感じですね.

## randomizedtestingでNoSuchMethodErrorが出る
```text:実行時出力
java.lang.NoSuchMethodError: com.carrotsearch.randomizedtesting.RandomizedContext.runWithPrivateRandomness(Lcom/carrotsearch/randomizedtesting/Randomness;Ljava/util/concurrent/Callable;)Ljava/lang/Object;
    __randomizedtesting.SeedInfo.seed([173369D720B9A36A:9F67560D8E45CE92]:0)
    org.elasticsearch.test.ElasticsearchIntegrationTest.buildWithPrivateContext(ElasticsearchIntegrationTest.java:583)
    org.elasticsearch.test.ElasticsearchIntegrationTest.buildAndPutCluster(ElasticsearchIntegrationTest.java:598)
    org.elasticsearch.test.ElasticsearchIntegrationTest.beforeInternal(ElasticsearchIntegrationTest.java:283)
    org.elasticsearch.test.ElasticsearchIntegrationTest.before(ElasticsearchIntegrationTest.java:1946)
    [...sun.*, org.junit.*, java.lang.reflect.*, com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:50)
    org.apache.lucene.util.TestRuleFieldCacheSanity$1.evaluate(TestRuleFieldCacheSanity.java:51)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleThreadAndTestName$1.evaluate(TestRuleThreadAndTestName.java:49)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)
```

単純に`randomizedtesting-runner`への依存を書き忘れている場合に加え, `randomizedtesting-runner`のバージョン指定が古い場合にも発生するようです. `elasticsearch 1.7.3`を使った際は, `randomizedtesting 2.1.6`ではこのエラーになり, `randomizedtesting 2.1.17`では正常に動きました.

## hamcrestでNoClassDefFoundErrorが出る
```text:実行時出力
java.lang.NoClassDefFoundError: org/hamcrest/Matchers
    __randomizedtesting.SeedInfo.seed([3F50D3975E9C4C58:B704EC4DF06021A0]:0)
    org.elasticsearch.test.ElasticsearchIntegrationTest.afterInternal(ElasticsearchIntegrationTest.java:628)
    org.elasticsearch.test.ElasticsearchIntegrationTest.after(ElasticsearchIntegrationTest.java:1954)
    [...sun.*, org.junit.*, java.lang.reflect.*, com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:50)
    org.apache.lucene.util.TestRuleFieldCacheSanity$1.evaluate(TestRuleFieldCacheSanity.java:51)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleThreadAndTestName$1.evaluate(TestRuleThreadAndTestName.java:49)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)
```

ドキュメントには書かれていなかったと思うのですが, hamcrestが必要なようです. `elasticsearch 1.7.3`を使った際は, `hamcrest 1.3`で動きました.

# プラグインが必要な機能のテスト
シンプルなテストは動いたでしょうか. 実は, ここからが本番です.
Elasticsearchで日本語を扱おうとした場合, 日本語アナライザなどのプラグインがないと何も始まりません. テストを走らせるために自前でElasticsearchサーバーを立ち上げていると, プラグイン周りの管理が面倒だったりするので, このあたりから`ElasticsearchIntegrationTest`のありがたみが出てきます.

サーバーとして立ち上がっているElasticsearchの中でプラグインを使おうとした場合, `$ bin/plugin --install <使用したいプラグイン>` とかを実行したと思います. 一方で, `ElasticsearchIntegrationTest`の中でプラグインを使う場合は,

1. 使用したいプラグインを依存関係に追加する.
2. `ElasticsearchIntegrationTest`クラスから継承した`nodeSettings`メソッドをオーバーライドして, プラグインを使用する旨を記述する.

の2つをやればOKです.

## 使用したいプラグインを依存関係に追加する
今回は日本語アナライザプラグインとしてお馴染みの`elasticsearch-analysis-kuromoji`を使ってみましょう. `pom.xml`に次の依存を追加します.

```xml:pom.xml
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch-analysis-kuromoji</artifactId>
    <version>2.7.0</version>
</dependency>
```

Elasticsearchとkuromojiプラグインとのバージョンの対応は, サーバーにインストールするときと同様に, [elasticsearch-analysis-kuromojiのGitHub](https://github.com/elastic/elasticsearch-analysis-kuromoji)で確認できます. 今回は`es-1.7`に対応する`elasticsearch-analysis-kuromoji 2.7.0`を使えばよいと分かります.

## nodeSettingsをオーバーライドしてプラグインを使用する旨を記述する
これには2通りやり方があります.

### クラスパスに存在するプラグインをすべて読み込む
```java:java:src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, true)
            .build();
    }
```

`"plugins."`に続けて`org.elasticsearch.plugins.PluginsService.LOAD_PLUGIN_FROM_CLASSPATH`で指定される文字列定数を連結した設定名に対して`true`を与えると, クラスパス上に存在するすべてのプラグインを読みに行くようになります.
ちなみに, `elasticsearch 1.7.3`ではこの設定名は`"plugins.load_classpath_plugins"`と展開されるようです.

### 読み込むプラグインを指定する
```java:java:src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .build();
    }
```

`"plugin.types"`にプラグインのクラス名を与えると, 与えた名前のプラグインが読み込まれます. テスト時に何のプラグインが必要なのかが明示されるので, 個人的にはこっちの方が好きです.

複数のプラグインを読み込ませたい場合は, `put`メソッドの代わりに`putArray`メソッドを使って次のように書きます.

```java:java:src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .putArray("plugin.types",
                AnalysisKuromojiPlugin.class.getName(),
                AnalysisICUPlugin.class.getName()) // elasticsearch-analysis-icu プラグイン
            .build();
    }
```

なお, `elasticsearch 2.1`からは`nodePlugins`というプラグインを指定するための専用メソッドをオーバーライドするように変更されているようです( [公式: integration tests (2.1)](https://www.elastic.co/guide/en/elasticsearch/reference/2.1/integration-tests.html#changing-node-configuration) ).

## 日本語を使ったテストケース
実際にkuromojiのプラグインが読み込めていることを確認してみましょう.

```java:src/main/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearch.java
package jp.akimateras.elasticsearch.esintegtest.service;

import static org.hamcrest.Matchers.is;

import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.plugin.analysis.kuromoji.AnalysisKuromojiPlugin;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Before;
import org.junit.Test;

@ElasticsearchIntegrationTest.ClusterScope(numDataNodes = 1)
public class SimpleSearchTest extends ElasticsearchIntegrationTest {
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .build();
    }

    @Before
    public void initializeIndex() throws Exception {
        // インデックス作成.
        XContentBuilder mapping = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(SimpleSearch.TYPE_NAME)
                    .startObject("properties")
                        .startObject(SimpleSearch.TITLE_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                            .field("analyzer", "kuromoji") // kuromojiがないとこれがエラーになる.
                        .endObject()
                        .startObject(SimpleSearch.TEXT_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                            .field("analyzer", "kuromoji")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();

        admin().indices().prepareCreate(SimpleSearch.INDEX_NAME)
            .addMapping(SimpleSearch.TYPE_NAME, mapping)
            .execute()
            .actionGet();

        // インデックス作成完了の待機.
        ensureGreen();
    }

    @Test
    public void testSearch() throws Exception {
        // ドキュメント投入. 日本語を使います.
        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "アルパカ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "アルパカは, 南アメリカのラクダ科動物の家畜です.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "リャマ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "リャマは, 南アメリカのラクダ科動物の家畜です.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "ビクーニャ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "ビクーニャは, 南アメリカのアンデス山脈に生息する草食哺乳類です.")
            .endObject());

        // ドキュメント投入完了の待機.
        flushAndRefresh();
        ensureGreen();

        // テスト用 client を使ってSimpleSearchインスタンスを生成.
        SimpleSearch service = new SimpleSearch(client());
        
        // テスト実施
        assertThat(service.search("アルパカ").size(), is(1));
        assertThat(service.search("リャマ").size(), is(1));
        assertThat(service.search("ビクーニャ").size(), is(1));
        assertThat(service.search("ラマ").size(), is(0));

        assertThat(service.search("家畜").size(), is(2));
        assertThat(service.search("アルパカ 家畜").size(), is(1));
        assertThat(service.search("リャマ 家畜").size(), is(1));
        assertThat(service.search("ビクーニャ 家畜").size(), is(0));
    }
}
```

どうでもいいんですが, このテスト, `kuromoji`を使わずともデフォルトの`standard analyzer`でも全部通ります...なかなか優秀ですね.
投入文書のタイトルとテキスト両方の`"アルパカ"`を`"アルパカアルパカ"`とかに変えてあげると, `standard analyzer`はこれを1語として認識するので, `"アルパカ"`で検索しても引っかからなくなります. `kuromoji`を使っている場合は`"アルパカ"``x2`と認識するので, `"アルパカ"`で検索できます.

# スクリプトが必要な機能のテスト
最後にgroovyとかのディスクスクリプトを必要とする機能のテスト方法を紹介します.

Elasticsearchのディスクスクリプトは, サーバーとして起動する場合は`/etc/elasticsearch/scripts`のような場所に置くと自動的に読み込まれるものでした. 
`ElasticsearchIntegrationTest`からスクリプトを見えるようにするには, サーバーのときと同様に特定のディレクトリにスクリプト群を設置し, `nodeSettings`メソッドで設置場所を記述すればOKです.

ただし, `elasticsearch 1.7.3`ではスクリプトディレクトリを直接指定することができません. `"path.conf"`で指定できる設定ディレクトリ直下の`scripts`という名前のディレクトリを決め打ちで探しに行きます.
そのため, 例えば`src/test/resources/scripts`というディレクトリを読みに行かせたい場合は, `"path.conf"`に`"src/test/resources"`を指定する, といったやり方になります.

```java:src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .put("path.conf", "src/test/resources")
            .build();
    }
```

なお. `elasticsearch 2.1`からは`"path.script"`という設定名でスクリプトディレクトリを直接指定できるように改善されているようです( [公式: Directory Layout (2.1)](https://www.elastic.co/guide/en/elasticsearch/reference/2.1/setup-dir-layout.html) ).

# おわりに
四苦八苦してテストフレームワークがやっと動いたよという記事ですので, 間違いなどありましたらご指摘を宜しくお願い致します...
苦労はしたものの, これのおかげでテスト用Elasticsearchサーバーの管理が不要になったのはかなり嬉しく, 導入した甲斐はあったと感じています.

明日は, [takakiku](http://qiita.com/takakiku)さんのMySQLスローログ可視化の記事です！