Elasticsearchを含むRspecのリクエストスペックを書いてみた


### 環境

railsとelasticsearchをそれぞれ別コンテナで動かしている

```yaml
services:
  elasticsearch:
    image: elastcserachのdockerイメージ
    (以下省略)
  rails:
    image: railsのdockerイメージ
    (以下省略)
```

### テスト用にGemを追加

テスト用のクラスタを別ポート（デフォルトは9250）で立ち上がることができる
https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-extensions

### テスト用のクラスタを立ち上げる

elasticsearchを使うときだけ立ち上げ、終了したら止める

```ruby
require 'elasticsearch/extensions/test/cluster'

before(:context) do
  Elasticsearch::Extensions::Test::Cluster.start(nodes: 1) unless Elasticsearch::Extensions::Test::Cluster.running?
end

after(:context) do
  Elasticsearch::Extensions::Test::Cluster.stop if Elasticsearch::Extensions::Test::Cluster.running?
end
```

実行したらエラーになる、、

```
Cannot find Elasticsearch launch script from [elasticsearch] -- did you pass a correct path?
```

elasticsearchとrailsのコンテナが違うので、railsのコンテナでelasticsearchを起動するには、railsのコンテナでelasticsearchを使えるようにしないといけないみたい。。。。

方向性を変えて、DB運用と同様に、テスト用のindexを用意する（先ほど追加したgemも削除）
ということで、alias_nameを環境ごとに分ける

```ruby
def alias_name
  "players_#{Rails.env}"
end
```

### 今回扱うデータ

Playerという選手のidと名前をカラムに持つテーブルのデータを扱う

|  id  |  name  |
| ---- | ---- |
|  1  |  xxxx  |
|  2  |  xxxx  |

下記のようにelasticsearchへ入れるデータを定義する

```ruby
require 'elasticsearch/model'

module SearchablePlayer
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    include Indexing

    index_name "players_#{Rails.env}_#{Time.now.strftime('%Y%m%d%H%M%S')}"

    settings do
      mappings dynamic: false do
        indexes :id, type: 'integer'
        indexes :name, type: 'string'
      end
    end
  end

  module Indexing
    def as_indexed_json(options={})
      {
        id: id,
        name: name,
      }
    end
  end

  module ClassMethods
    def alias_name
      "players_#{Rails.env}"
    end
  end
end
```

### テスト用のクラスタにデータを入れる

rspec内にテーブルの中身をelasticsearchへ反映するためのメソッドを定義する
（elasticsearchへの反映には時間がかかるので、1秒sleepを入れる）
このとき、index名は「players_test」となるので、開発環境のindexには影響はない

```ruby
def elasticsearch_update
  index_name = Player.index_name
  alias_name = Player.alias_name
  
  # Playerのindexを追加
  Player.__elasticsearch__.import(
    force: true,
    index: index_name
  )
  
  # Playerのindexを追加
  actions = [{
    add: {
      index: index_name,
      alias: alias_name
    }
  }]
  Player.__elasticsearch__.client.indices.update_aliases(
    body: { actions: actions }
  )
  # インデックスに反映するために1秒待つ
  sleep 1
end
```

### リクエストスペックを書く

今回は選手の一覧を返却するAPIのテストを書く
beforeでテーブルにレコードを作成してから、先ほど定義したメソッドを呼ぶ

```ruby
describe '選手の一覧を返却するAPI'
  before do
    # データを作成する
    FactoryBot.create(players, name: 'test')
    # 作成したデータをelasticsearchへ反映させる
    elasticsearch_update
  end
  it do
    get 選手一覧のAPIパス
    expect(レスポンスの選手名).to eq(test)
  end
end
```

### テストが終わったらindexを削除する

このままだとテストを回すごとにindexが追加されていってしまうので、
テストが終わったらindexを削除する
また、elasticsearchへデータを投入しないパターンもあるので、
その場合にNotFoundの例外が起きるので、rescueする（もっといい方法があるかも、、）

```ruby
after(:example) do
  player_client = Player.__elasticsearch__.client
  alias_name = Player.alias_name
  begin
    delete_indices = player_client.indices.get_alias(name: alias_name).keys
    player_client.indices.delete(index: delete_indices)
  rescue Elasticsearch::Transport::Transport::Errors::NotFound
  end
end
```

### まとめ

gem（Elasticsearch::Extensions）が使えなかったが
DB運用のように、テスト用のindexを作ることで十分にテストを書くことができた。（特に弊害もなさそう？）

### 参考

https://techlife.cookpad.com/entry/2015/09/25/170000
https://blog.bitjourney.com/entry/2015/05/22/162250