Capybara::ElementNotFound 回避に wait させるか retry させるか

CapybaraPhantomJS (Poltergeist) を使い Ajax 処理をテストする際に、結果取得が間に合わずにテストに失敗するケースがあり悩んでいました。

Failure/Error: find('.alert-success', text: "Success!")

Capybara::ElementNotFound:
  Unable to find css ".alert-success" with text "Success!"

TL;DR

  • capybara の wait 時間を延ばしたが解決しなかった
  • 特定のテストを再実施する rspec-retry を使ったところ問題が解決した

Capybara は非同期処理とどう向き合っているのか

Capybara の README にはこんな記述があります。

Capybara is smart enough to retry finding the link for a brief period of time before giving up and throwing an error.

実際の処理はCapybara::Node::Base#synchronize で行われているようです。ここには更に詳細なコメントがありました。

As long as any of these exceptions are thrown, the block is re-run, until a certain amount of time passes. The amount of time defaults to {Capybara.default_max_wait_time} and can be overridden through the seconds argument.

非同期処理が終わるまでリトライしてもらう方法

Capybara.default_max_wait_time のデフォルト値は2秒に設定されているので、こいつを延ばせば良さそうです。

方法1. グローバル設定値を変更する

spec_helper.rb に以下を追記してみます。

Capybara.default_max_wait_time = 5

方法2. オプション引数を渡す

Capybara::Node::Finders のメソッドはオプション引数を受け付けています。この中に wait というものがあります。

Module: Capybara::Node::Finders — Documentation for jnicklas/capybara (master)

find('.alert-success', text: "Success!", wait: 5)

一度はこの手法を採用したのですが、それでも数回に一回失敗していました。徐々に閾値を上げていったのですが、 wait: 30 でもまだ失敗するので別の方法を模索しました。

方法3. rspec-retry で特定のテストを再実施する

そこで思いついたのが特定のテストを再実施する方法です。過去に別プロジェクトで利用実績のあった NoRedInk/rspec-retry を採用しました。

なお、alternative として dblock/rspec-rerun というものがあるようです。

どの方法が良いのか

必ず失敗するテストを用意し、実施時間をざっくり計測してみました。

1. デフォルト値をそのまま利用

Failure/Error: find('.alert-info', text: "Success!")

Capybara::ElementNotFound:
  Unable to find css ".alert-info" with text "Success!"

Finished in 7.38 seconds (files took 5.33 seconds to load)
1 example, 1 failure

=> ロード時間 + デフォルト待ち時間(2秒)程度でテストが落ちました。

2. グローバル設定値を変更

Failure/Error: find('.alert-info', text: "Success!")

Capybara::ElementNotFound:
  Unable to find css ".alert-info" with text "Success!"

Finished in 9.81 seconds (files took 5.38 seconds to load)
1 example, 1 failure

=> ロード時間 + 待ち時間(5秒)程度でテストが落ちました。

3. オプション引数を利用

Failure/Error: find('.alert-success', text: "Success!", wait: 5)

Capybara::ElementNotFound:
  Unable to find css ".alert-success" with text "Success!"

Finished in 9.73 seconds (files took 5.43 seconds to load)
1 example, 1 failure

=> ロード時間 + 待ち時間(5秒)程度でテストが落ちました。

4. rspec-retry を利用

1st Try error in ./spec/features/sample.feature:7:
Unable to find css ".alert-success" with text "Success!"

RSpec::Retry: 2nd try ./spec/features/sample.feature:7

2nd Try error in ./spec/features/sample.feature:7:
Unable to find css ".alert-success" with text "Success!"

RSpec::Retry: 3rd try ./spec/features/sample.feature:7
    Step 1 -> Step 2 -> Step 3 (FAILED - 1)

Failures:

  1) Sample Step 1 -> Step 2 -> Step 3
     Failure/Error: find('.alert-success', text: "Success!")

     Capybara::ElementNotFound:
       Unable to find css ".alert-success" with text "Success!"

Finished in 12.03 seconds (files took 5.39 seconds to load)
1 example, 1 failure

=> ロード時間 + 待ち時間(2秒)* リトライ数(3回) 程度でテストが落ちました。

最終的にどうしたか

rspec-retry を採用しました。2秒待って結果が返ってこないのであれば、それ以上待つより再実施した方が早いだろうという判断です。これでしばらく運用してみようと思います。