不安定テストを生み出すCapybaraを調教する
2020/06/12
@銀座Rails #22
Masatoshi Iwasaki
自己紹介
Masatoshi Iwasaki
masa-iwasaki
masa_iwasaki
フリーランスエンジニア
今日のテーマ
RailsでCapybaraを使ったテストが不安定なときに
「とりあえずこれ試して!」
というTipsについてまとめました
(「もうやっているよ!」ということも多いかも...)
対象とする環境
話さないこと
約束できないこと
(一応)Capybaraとは
“Capybara is a library written in the Ruby programming language which makes it easy to simulate how a user interacts with your application.”
不安定テストとは?
(本発表での)不安定テストの定義
ほんとは通る(greenになる)はずなのに、
なぜか原因不明で落ちる(redになる)テストのこと。
つまり、テスト自体は正しく書けている(はず)前提。
稀に「ほんとは落ちるべきだったのにほとんどのケースで通っていた」という悲しい事実が発覚することもある
不安定テストの発生要因
不安定テストの発生要因
今日のメイントピック
落ちづらいテストを書かない方法
落ちづらいテストを書かないために
findの実装
def find(*args, **options, &optional_filter_block)
options[:session_options] = session_options
synced_resolve Capybara::Queries::SelectorQuery.new(*args, **options, &optional_filter_block)
end
findメソッドのコードは以下のようになっている。
def synchronize(seconds = nil, errors: nil)
return yield if session.synchronized
seconds = session_options.default_max_wait_time if [nil, true].include? seconds
session.synchronized = true
timer = Capybara::Helpers.timer(expire_in: seconds)
begin
yield
rescue StandardError => e
session.raise_server_error!
raise e unless catch_error?(e, errors)
if driver.wait?
raise e if timer.expired?
sleep(0.01)
reload if session_options.automatic_reload
else
old_base = @base
reload if session_options.automatic_reload
raise e if old_base == @base
end
retry
ensure
session.synchronized = false
end
end
個々の要素を
追っていくと結構複雑
簡略化
begin
セレクタを探すクエリの実行
rescue StandardError => e
raise e if 例外がElementNotFound以外
raise e if 制限時間超えている
sleep(0.01)
retry
...
大筋に関係ない処理を抜いてざっくりな挙動を抽出するとこんな感じ
findの挙動まとめ
落ちづらいテストを書かないために
findの挙動まとめ(再掲)
findの隙を突かれるケース
1)セレクタの指定が不十分
# Bad:
expect(find(".user")["data-name"]).to eq("Joe")
# Good:
expect(page).to have_css(".user[data-name='Joe']")
Badのコードでは最初に見つかった .user なエレメントを返してくるが、datasetが期待する値でないことがある。
上記の例は .user な要素がもともとあり、その要素のdata属性に name='Joe' が追加されることを想定しているケース。JSの実行時間が長い、もしくはサーバーサイドのレスポンスが遅いなどの理由でdata属性への変更が起こる前にクエリが実行されてしまうとテストが失敗する。
Goodのコードを使えばfindが要素が見つかるまで一定時間待ってくれる。
2)findと異なる待ち方をするfinderメソッド
このあたりの元ネタ
3)制限時間を超える実行時間
# spec/rails_helper.rb などでsystem spec全体で待ち時間を延ばす
Capybara.default_max_wait_time = 5
# 特定の要素について秒数指定で待つ
find '.something', wait: 10
# 特に根拠無く待ち時間を延ばしたい場合は整数値を掛けると
# 「適当に増やしてるんだな」感がでるかもしれない
find '.something-red', wait: Capybara.default_max_wait_time * 3
落ちづらいテストを書かないために
個人的な不安定テストの頻出例
ブログ記事登録のsystem specで登録してからDBの中身をチェックするようなケース
# system specの一部
fill_in "タイトル", with: "CI不安定で切ない"
click_on "登録"
# 登録ボタンを押したときに非同期処理だとBlogEntryの追加が完了していない可能性があるが、
# 開発環境だと高速に処理が行われるので失敗することがほとんどない
expect(BlogEntry.last.title).to eq "CI不安定で切ない"
※system specでActiveRecordインスタンスの値をチェックすることの是非に議論はあるかもしれないが、本発表では対象外とする。
ユーザーへの表示を待つ
flashなどで表示されるメッセージやメッセージが含まれる要素を待つことで、DBにデータがある状態が保証される
fill_in "本文", "CI不安定で切ない"
click_on "登録"
expect(page).to have_css(".flash.notice")
expect(page).to have_content("登録が完了しました")
expect(BlogEntry.last.body).to "CI不安定で切ない"
疑問
ユーザ通知がすぐ消える(見えなくなる)ときも大丈夫?
findの挙動まとめ(また出たな!)
ユーザーに対する確認表示をひたすらチェック
セレクタ力を高めて不安定テストを減らす
sleep() less
sleep()は再現手法として使う
wait_for_ajaxメソッドについて
Seleniumの詳細な動作を知りたい場合
Selenium::Webdriver::Loggerが環境変数のDEBUGを見て標準出力に詳細ログを出してくれる(!?)
これは...
DEBUG=true bin/rspec path/to/some_spec.rb
(おまけ)
不安定テスト対策の便利設定
不安定テストの防止・検出に役立つ設定
特にCI環境ではやっておきたい。
CSSアニメーションを切る
CSSアニメーションの実行速度が影響してテスト実行結果が不安定になることを防止する
# in spec/rails_helper.rb
Capybara.disable_animation = true
Capybara::Server::AnimationDisabler でHTMLヘッダーにアニメーションを止めるスタイル指定を埋め込んでいる。
この設定では不十分という場合は上記設定を使わず、自前でテスト実行時に使うアニメーション無効化するためのCSSを用意してtest実行時だけそれを読み込むようにすれば良い。
assets:precompileを先に走らせる
# CI上で bin/rspec 実行前に走らせる
RAILS_ENV=test bin/rails assets:precompile
最初のsystem specが実行される前にassets:precompileを走らせておき、初回に実行されるテストでのロード時間延長などによる不安定な結果やタイムアウトを防ぐ。
CI上でassets:precompileを実行するため、staging/production環境にデプロイする前の動作確認にもなる。
(オプション)
config.assets.compileの無効化
# in config/environments/test.rb
if ENV[‘CI’]
config.assets.compile = false
end
加えて、config.assets.compileを無効化することでassets:precompileした後で参照できないassetsがあるとそのままテストが落ちてくれる。
ブラウザコンソールのログをチェックする
capybara-chromedriver-logger
READMEにある画像(以下)のようにブラウザのコンソールに表示されるログが出る。
ここからcapybara-chromedriver-logger を使った場合のtipsを紹介。使わない場合でもログ出力する場合は共通する話のはず。
ログのノイズを減らす
Vue.config.devtools = false;
Vue.config.productionTip = false;
運用上、通常は一切ログがでないことを前提として、ログが出たらすべてfailさせるようにしないと、CIの実行時ログを追わないといけないため、気づきづらい。
たとえばVue.jsだとDevToolsやproduction向けのtipsなどが表示されるので、assets:precompileの時点などで表示しないようにしておきたい。
filterを活用する
# spec/rails_helper.rb
Capybara::Chromedriver::Logger.filters = [
/the server responded with a status of 422/
]
JSライブラリが出すログを抑制するオプションがなかったり、HTTPステータスコードで4xxが返ってくるケース等ではログが出てしまうのでfilterを使って抑制する。
rspecのexample毎に制御したい仕組みを作る場合はbefore/after hooksを組み合わせたりと追加の手間がかかる。
本日のまとめ
不安定テストの防止・検出に役立つ設定
落ちづらいテストを書かないためのポイント
最後に
本日のタイトル
不安定テストを生み出すCapybaraを調教する
🙄❓
Capybara全然悪くない!!
調教が必要なのは我々だった
ご清聴ありがとうございました