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


環境

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

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

テスト用にGemを追加

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

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

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

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を環境ごとに分ける

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

今回扱うデータ

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

id name
1 xxxx
2 xxxx

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

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には影響はない

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でテーブルにレコードを作成してから、先ほど定義したメソッドを呼ぶ

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する(もっといい方法があるかも、、)

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