Capybara と PhantomJS (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.
Asynchronous JavaScript (Ajax and friends)
実際の処理は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/base.rb
非同期処理が終わるまでリトライしてもらう方法
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)
方法3. rspec-retry で特定のテストを再実施する
上記のオプション引数を渡す方法を使っていたのですが、それでも数回に一回失敗していました。徐々に閾値を上げていったのですが、 wait: 30
でもまだ失敗するので別の方法を模索しました。
そこで思いついたのが特定のテストを再実施する方法です。過去に別プロジェクトで利用実績のあった NoRedInk/rspec-retry を採用しました。
なお、alternative として dblock/rspec-rerun というものがあるようです。
どの方法が良いのか
必ず失敗するテストを用意し、実施時間をざっくり計測してみました。
1. デフォルト値をそのまま利用
Failure/Error: find('.alert-info', text: "foobar!")
Capybara::ElementNotFound:
Unable to find css ".alert-info" with text "foobar!"
Finished in 7.38 seconds (files took 5.33 seconds to load)
1 example, 1 failure
=> ロード時間 + デフォルト待ち時間(2秒)程度でテストが落ちました。
2. グローバル設定値を変更
Failure/Error: find('.alert-info', text: "foobar!")
Capybara::ElementNotFound:
Unable to find css ".alert-info" with text "foobar!"
Finished in 9.81 seconds (files took 5.38 seconds to load)
1 example, 1 failure
=> ロード時間 + 待ち時間(5秒)程度でテストが落ちました。
3. オプション引数を利用
Failure/Error: find('.alert-info', text: "foobar!", wait: 5)
Capybara::ElementNotFound:
Unable to find css ".alert-info" with text "foobar!"
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-info" with text "foobar!"
RSpec::Retry: 2nd try ./spec/features/sample.feature:7
2nd Try error in ./spec/features/sample.feature:7:
Unable to find css ".alert-info" with text "foobar!"
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-info', text: "foobar!")
Capybara::ElementNotFound:
Unable to find css ".alert-info" with text "foobar!"
Finished in 12.03 seconds (files took 5.39 seconds to load)
1 example, 1 failure
=> ロード時間 + 待ち時間(2秒)* リトライ数(3回) 程度でテストが落ちました。
最終的にどうしたか
4番の方法を採用しました。2秒待って結果が返ってこないのであれば、それ以上待つより再実施した方が早いだろうという判断です。これでしばらく運用してみようと思います。