Capybara + Selenium の Flaky Test に抗う
RSpec + Capybara + Selenium で Headless Chrome による System Spec を実行しているプロジェクトで、たまに失敗する Flaky Test が存在する。
テストの内容は、ページ内の「保存」ボタンをクリックして、ページ遷移後に「変更を保存しました。」メッセージが表示されることをチェックするもの。 文章にするとシンプルなのだが、このテストがたまに失敗する。リトライすると大抵成功する。ページ遷移する前にテストが評価されているのかと思い、wait で待機してみたものの解決しなかった。
調査する時間が取れず根本的に解決するのが難しいため、失敗したらリトライするモジュールを作成した (Claude Code が)。
retry_on_stale_element ブロック内のテストが失敗したとき、エラークラス (Selenium::WebDriver::Error::UnknownError) とエラーメッセージ (Node with given id does not belong to the document) であれば 0.1 秒待ってリトライする。デフォルトでは 5 回までリトライする。
# spec/support/capybara_retry.rb
module Capybara
module Retry
# Retries a block of code when a specific Selenium "stale element" error occurs.
# This is useful for handling race conditions in feature specs where the DOM
# is updated while Capybara is performing an action.
#
# @param max_retries [Integer] The maximum number of times to retry.
# @param wait_time [Float] The wait time in seconds before retry.
# @yield The block of Capybara operations to execute.
def retry_on_stale_element(max_retries: 5, wait_time: 0.1)
retries = 0
begin
yield
rescue ::Selenium::WebDriver::Error::UnknownError => e
# Only retry for this specific "stale element" message from Selenium
raise e unless e.message.include?('Node with given id does not belong to the document')
if retries < max_retries
retries += 1
warn "WARN: Stale element detected. Retrying ##{retries}/#{max_retries} after #{wait_time}s..."
sleep wait_time
retry
else
warn "ERROR: Stale element error persisted after #{max_retries} retries."
raise e
end
end
end
end
end必要に応じて rails_helper.rb などで require する。
# spec/rails_helper.rb
Rails.root.glob('spec/support/**/*.rb').each { |f| require f }System Spec などで以下のように使用する。
it 'example request spec', :js do
click_on '保存'
retry_on_stale_element do
expect(page).to have_content '変更を保存しました。'
end
endこういったテストが CI で失敗すると残念な気持ちになるが、今回の対応でストレスが減った。 Playwright は wait で待機する必要がないらしいので、余裕ができたら移行したい。