RSpec Elasticsearch テスト設定:metadataで特定のテストだけインデックスを初期化する

7分で読めるテック

elasticsearch-rails を使う Rails アプリのテストを書いていて、Elasticsearch のインデックス初期化をどのタイミングで走らせればいいか悩んだことがありました。

全テストで毎回初期化すると実行時間が増える。かといって初期化しないとテスト間でインデックスが干渉して、対象メソッドは正しいのに意図しない結果が返ってくる。この記事では RSpec の metadata を使って「Elasticsearch を使うテストだけ初期化する」設定を紹介します。

なぜインデックス干渉が起きるのか

Elasticsearch のインデックスはテストをまたいで共有されます。別のテストで追加・更新されたドキュメントがそのまま残るため、後続のテストが想定外のデータを拾ってしまいます。

RDB では DatabaseCleaner などでトランザクションロールバックを使えますが、Elasticsearch にはトランザクションがないので、テストごとにインデックスを作り直す必要があります。ただ、Elasticsearch を使わないテストまで初期化を走らせると無駄なオーバーヘッドが生じます。

参考: Elasticsearch の公式インデックス作成ドキュメント

設定手順

1. 接続先を環境変数で管理する

テスト環境の Elasticsearch コンテナに接続するホストとポートを config/settings/test.yml(config_for や config gem を使う構成の場合)で環境変数から取れるようにします。

# config/settings/test.yml
elasticsearch:
  host: <%= ENV['ELASTICSEARCH_HOST'] || 'localhost' %>
  port: <%= ENV['ELASTICSEARCH_PORT'] || '9200' %>

2. テスト環境でクライアントを初期化する

config/initializers/elasticsearch.rb などで、テスト環境でも Elasticsearch クライアントを初期化します。elasticsearch-ruby のクライアント設定の詳細は下記を参照してください。

Elasticsearch::Model.client = Elasticsearch::Client.new(
  hosts: [
    {
      host: Settings.elasticsearch.host,
      port: Settings.elasticsearch.port
    }
  ],
  log: true
) if Rails.env.test?

3. spec_helper.rb に metadata で分岐する初期化処理を追加する

spec_helper.rbRSpec.configure ブロック内に、elasticsearch: true という metadata が付いたテストだけインデックスの作成・削除を行う処理を追加します。

# spec/spec_helper.rb
config.before(:each) do |example|
  Product.__elasticsearch__.create_index! if example.metadata[:elasticsearch]
end

config.after(:each) do |example|
  if example.metadata[:elasticsearch] && Product.__elasticsearch__.index_exists?
    Product.__elasticsearch__.delete_index!
  end
end

before でインデックスを作り、after で存在確認してから削除する流れです。index_exists? のチェックを入れておくと、テスト失敗時にインデックスが作られていなかった場合のエラーを防げます。

4. テストに metadata を付ける

あとは Elasticsearch を使うテストに elasticsearch: true を付けるだけです。

RSpec.describe SampleClass, type: :model, elasticsearch: true do
  it "検索結果が正しく返る" do
    # ...
  end
end

elasticsearch: true がないテストは before/after の初期化処理をスキップするので、Elasticsearch を使わないテストのパフォーマンスに影響しません。

GitHub Actions で CI に組み込む

この設定をそのまま GitHub Actions に持ち込めます。services に Elasticsearch コンテナを定義して、テスト実行時に環境変数 ELASTICSEARCH_PORT を渡すだけです。

# .github/workflows/test.yml
jobs:
  rspec:
    services:
      elasticsearch:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.16.3
        env:
          discovery.type: single-node
        ports:
          - 9200:9200
    steps:
      - name: run rspec
        env:
          ELASTICSEARCH_PORT: ${{ job.services.elasticsearch.ports['9200'] }}

バージョン注記: この設定は Elasticsearch 7.16.3 を前提にしています。8.x 系ではセキュリティ設定(TLS・認証)がデフォルトで有効になっているため、CI で使う場合は xpack.security.enabled: 'false' などの追加設定が必要になることがあります。

バージョン前提

この記事のコードは RSpec 3.x / Rails 6.x を前提に書かれています。config.before(:each) / config.after(:each) はRSpec 3系で安定している構文です。RSpec 2系以前では一部のAPIが異なります。RSpecの各バージョンのドキュメントはこちらを参照してください。

参考

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。

このサイトについて

井上 周(Amane Inoue)の個人ブログです。技術・読書・ドラマ・旅・大学生活のことを書いています。