dreamin' blog

randomにテストが落ちてしまう

どんな種類のテストでもrandomにテストは落ちてしまうことがあります。
Ruby on Railsを利用している場合にもっともこのような不安定なテスト (flaky test) になりがちなのは、Capybaraを利用したsystem test/system specではないでしょうか。
私がrandomに落ちるテストと戦った中で得た経験をここに記しておきます。

基本的に自前で sleep はしなくて良い

ページ遷移の直後や描画された直後にclick_linkやclick_buttonなどを実行すると、ブラウザの描画よりもブラウザを操作するテストの実行が早く走ってしまいrandom落ちを引き起こします。
そういうときの解決策として sleep 0.5 などとして、テストの実行を止めるアイデアを思いつくでしょう。

しかし、capybaraは Capbypara.default_max_timeout までは、対象のコンテンツを探してくれるメソッドが用意されています。
ですので、基本的に自前でsleepする必要はありません。

例をいくつか挙げておきます。

click_link '次へ' # ここでrandom落ちしたら
within '.some_dom' do # 次への親要素を探すまで待たせる
  click_link '次へ'
end
find('.next').click # 次へのリンクを探してからクリックする
expect(page).to have_content 'some content' # 画面遷移の直後の場合は expect も有効です
click_link '次へ'

within / find で超えられない壁

find('.some_dom').click # .next_domが表示される
find('.next_dom').click

このようにある操作をしてから、表示するコンテンツを操作する場合、capybaraの操作ブラウザの表示より早く、.next_dom が表示する前に .next_dom をfindして見つからずにエラーになることがあります。

find('.some_dom').click
Timeout.timeout(Capybara.default_max_wait_time) do
  loop until find('.next_dom', visible: false).visible?
end
find('.next_dom').click

上記のようにすることで、 .next_dom が必ずある状態を担保してからクリックすることができます。
ここで安易にsleepしてしまうと、時間がもったいないのでCPUのスピードでloopさせるというのがポイントです。

ただ Capybara.default_max_wait_time が長い時間設定されていると、本当に落ちるべきテストが落ちるまでの時間が長くなります。 --fail-fast なども活用しながら、できる限り短い時間を設定したほうがCIのスピードには寄与するでしょう。

楽観的UIの壁

楽観的UIとはクリックした操作のサーバー側の成否に関わらずユーザーにUI操作を続けさせるというものです。

というようなものです。

普通のUI操作のテストでは、サーバーサイドの変化については問題になりません。
しかし、サーバーサイドで発番されたデータなどを確認しないと、次のテストに移れいないようなテストもあります。
(本来テストとして別のシナリオにわけるべきですが、生成されるデータ複雑な場合などはfixtureを用意するコストが高い場合もあります)

click_button '投稿'
expect(page).to have_content 'thank you.'' # とりあえず投稿ボタンが押されたら表示される、成功したら裏でメールが送られる
visit post_path(Post.find(token: 'now_token')) # メールに乗っているリンクを押すには投稿の裏で生成されたpostが必要

そのような場合は wait_for_ajax のようなメソッドを作りましょうというのが慣例となっています。自分はjqueryが必要なこのギミックがあまり好きではないのですが、楽観的UIとサーバーサイドのデータを合わせ技で確認しないとできないテストでは仕方なく使いました。

click_button '投稿'
expect(page).to have_content 'thank you.'' # とりあえず投稿ボタンが押されたら表示される、成功したら裏でメールが送られる
wait_for_ajax # ajaxリクエストが終わるまで待つ
visit post_path(Post.find(token: 'now_token')) # メールに乗っているリンクを押すには投稿の裏で生成されたpostが必要
def wait_for_ajax
  Timeout.timeout(Capybara.default_max_wait_time) do
    loop until page.evaluate_script('jQuery.active').zero?
  end  
end

そもそも動いているDOMは表示されていてもクリックできないという壁

後述する参考文献をあたって欲しいのですが、そもそも動いているDOMはクリックできません。そこであまりにも動きが多いページのテストは以下を設定することで、解決するかもしれません。

Capybara.disable_animation = true

それでもクリックできない (sleep の方がましかも)

それでもclickに失敗することがあったので、こういうギミックに改良したりしました。。。画面先するまで押したる!ということです。

wait_for_path_change('next_path') { click '次へ' }
find('.next_path_dom').click
def wait_for_path_change(path, wait_time = Capybara.default_max_wait_time)
Timeout.timeout(wait_time) do
  yeild if block_given?
  loop until page.current_path == path
rescue TimeoutError => e
  pp e
  if block_given?
    yield 
  else
    raise e
  end
end

だいぶバッドノウハウになってきました。
素直に sleep 0.5 とした方が全体的なメンテのしやすさは秒数でできるので便利かもしれません。。。

まとめ

他にもランダム落ち。特にsystem specやjsを有効にしたfeature specでは苦しめられることが多いと思います。このように知見がたくさんでてくると嬉しいので、自分からシェアしてみることにしました。

補足(というかメモ)

ランダム落ちとの戦いのときは、、以下のコマンドを多用しました。

$ RSPEC_RETRY_RETRY_COUNT=0 rspec --fail-fast --order=defined

参考文献