RSpec Elasticsearch テスト設定:metadataで特定のテストだけインデックスを初期化する
elasticsearch-rails を使う Rails アプリのテストを書いていて、Elasticsearch のインデックス初期化をどのタイミングで走らせればいいか悩んだことがありました。
全テストで毎回初期化すると実行時間が増える。かといって初期化しないとテスト間でインデックスが干渉して、対象メソッドは正しいのに意図しない結果が返ってくる。この記事では RSpec の metadata を使って「Elasticsearch を使うテストだけ初期化する」設定を紹介します。
なぜインデックス干渉が起きるのか
Elasticsearch のインデックスはテストをまたいで共有されます。別のテストで追加・更新されたドキュメントがそのまま残るため、後続のテストが想定外のデータを拾ってしまいます。
RDB では DatabaseCleaner などでトランザクションロールバックを使えますが、Elasticsearch にはトランザクションがないので、テストごとにインデックスを作り直す必要があります。ただ、Elasticsearch を使わないテストまで初期化を走らせると無駄なオーバーヘッドが生じます。
参考: Elasticsearch の公式インデックス作成ドキュメント
インデックスの作成 | Elasticsearchリファレンス [5.4] | Elastic
設定手順
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 のクライアント設定の詳細は下記を参照してください。
GitHub - elastic/elasticsearch-ruby: Ruby integrations for Elasticsearch
Ruby integrations for Elasticsearch. Contribute to elastic/elasticsearch-ruby development by creating an account on GitHub.
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.rb の RSpec.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の各バージョンのドキュメントはこちらを参照してください。

RSpec documentation
参考
blog.bitjourney.com/entry/2015/05/22/162250

RSpecで独自metadataを利用する - Qiita
RSpec の独自 metadata が便利だったので、利用方法と思い付く使用ケースをまとめます。 環境 bundle exec rspec -v RSpec 3.9 - rspec-co...
記事の更新をメールで受け取る
質問・リクエストを送る
記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。