Upgrade to Pro — share decks privately, control downloads, hide ads and more …

不安定テストを生み出すCapybaraを調教する

Masatoshi Iwasaki
June 12, 2020
1k

 不安定テストを生み出すCapybaraを調教する

銀座Rails#22での発表資料です。

https://ginza-rails.connpass.com/event/176491/

Masatoshi Iwasaki

June 12, 2020
Tweet

Transcript

  1. • rspecを対象としてます ◦ minitestでも役に立つことはあるかもしれません • Capybara + SeleniumDriver前提 ◦ webdrivers

    gem使ってるならこのパターン ◦ headless chrome以外の経験が無いのでFirefoxとか他のブラウ ザだと違うこともあるかもしれません • system spec前提 ◦ feature spec使ってる場合は適宜読み替えてください 対象とする環境
  2. (一応)Capybaraとは “Capybara is a library written in the Ruby programming

    language which makes it easy to simulate how a user interacts with your application.” http://teamcapybara.github.io/capybara/ • ざっくり書くと、いい感じにブラウザ上で ユーザーが行う操作を記述&実行する ためのライブラリ • Seleniumなどの外部ライブラリを driver として利用することができ、 Capybaraが それらを使うための便利メソッドを提供し てくれている
  3. 不安定テストの発生要因 • 開発環境とCI環境の違い ◦ ハードウェア・VM環境の違い ◦ OS ◦ 利用しているライブラリのバージョン ◦

    テストの実行順序(rspec --rand) • 落ちる可能性のあるテストの書き方 • 原因不明 ◦ 調べてもよくわからないケース
  4. 不安定テストの発生要因 • 開発環境とCI環境の違い ◦ ハードウェア・VM環境の違い ◦ OS ◦ 利用しているライブラリのバージョン ◦

    テストの実行順序(rspec --rand) • 落ちる可能性のあるテストの書き方 • 原因不明 ◦ 調べてもよくわからないケース 今日のメイントピック
  5. 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メソッドのコードは以下のようになっている。 • 引数はすべてCapybara::Queries::SelectorQuery へと引き渡される ◦ find_buttonなどのfind_系メソッドはfindのラッパー ◦ 複雑なクエリの処理をすべて引き受けてくれるのでこれ以降のコードを追うのが楽 • Capybara::Node::Finders#synced_resolve から Capybara::Node::Base#synchronize へと処 理が進む
  6. 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 個々の要素を 追っていくと結構複雑
  7. 簡略化 begin セレクタを探すクエリの実行 rescue StandardError => e raise e if

    例外がElementNotFound以外 raise e if 制限時間超えている sleep(0.01) retry ... 大筋に関係ない処理を抜いてざっくりな挙動を抽出するとこんな感じ
  8. 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が要素が見つかるまで一定時間待ってくれる。
  9. 2)findと異なる待ち方をするfinderメソッド • Capybara::Node::Finders#all ◦ find_allはallのエイリアス ◦ firstも内部でallを呼んでいる • クエリ生成はfindと一緒だが、クエリ実行時にElementNotFoundを raiseすることはない

    ◦ allの場合は何もマッチしないと空配列を返す ◦ firstは1つも該当しなかったら例外を出すが、1つでも見つかったらそ れでOK • 「all使ったから指定したセレクタの要素が全部返ってくる」という保証はな い
  10. • Write Reliable, Asynchronous Integration Tests With Capybara ◦ 2014年に書かれた記事(最終更新は2019年)

    ◦ 先ほどのbad/goodのコードは本記事から引用 • この記事ではfind等の待ち方に関する解説が薄く、紹介されている bad/goodの違いがわかりづらいのが難点だなと感じていた ◦ 実はこれが今回発表しようと思ったきっかけ • さすがに古くなっていて挙動が最新と違う点もあるが、bad/goodコードは 今でも使えるノウハウ ◦ 不安定なテストに遭遇したらいつも読み直してます このあたりの元ネタ
  11. 3)制限時間を超える実行時間 • Capybara.default_max_wait_time ◦ これがfindがクエリ実行を繰り返す場合の制限時間(待ち時間) ◦ デフォルトで2秒(秒単位の数値で指定) • 2秒では終わらない処理があるような場合、この待ち時間を伸ばす #

    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
  12. ブログ記事登録のsystem specで登録してからDBの中身をチェックするよう なケース 個人的な不安定テストの頻出例 # system specの一部 fill_in "タイトル", with:

    "CI不安定で切ない " click_on "登録" # 登録ボタンを押したときに非同期処理だと BlogEntryの追加が完了していない可能性があるが、 # 開発環境だと高速に処理が行われるので失敗することがほとんどない expect(BlogEntry.last.title).to eq "CI不安定で切ない " ※system specでActiveRecordインスタンスの値をチェックすることの是非に議論はあるかもしれない が、本発表では対象外とする。
  13. findの挙動まとめ(また出たな!) • findは渡されたセレクタにマッチする要素を見つけるためのクエリを作る • まずクエリを実行してElementNotFound以外の例外が来たらそのまま raiseする • ElementNotFoundで制限時間(後述)以内だったら0.01秒寝てから再 度クエリを投げることを繰り返す •

    制限時間を超えていたら例外をraiseする ◦ 最後の実行がそれまで通り終了したらElementNotFoundをraise することになる • 登録処理が非同期であればこの初回クエリ実行のほうが flashが表示される(サーバーサ イドの処理が終わる)より先に実行される可能性のほうが高い。 • (待ち時間超えない限りは)flashが表示されるまでfindが待ってくれる。 • 人間が認知するために必要な表示時間はコンピュータに取っては十分すぎる時間
  14. sleep() less • ここまで紹介してきた事項を組み合わせることでsleep()は消せる • 「そもそもsleep使わなくていいのが普通」のほうがよくて、頻繁に利用するも のではないという認識が良さそう • 参考資料:ちょうどよいRails E2E

    test ◦ アプローチによりsleep使ってないという実例 ◦ 不安定テスト対策については言及されていないが、ユーザー目線での 実装についてはアプローチが共通していて、かつこちらのほうがより説 明が充実している
  15. wait_for_ajaxメソッドについて • Automatically Wait for AJAX with Capybara で紹介されている手法 ◦

    古くからあるRailsプロジェクトだと実装されているかも • jQuery前提の実装 ◦ かつ、jQueryをサーバーサイドとの通信に使っていなければ使えない ◦ そもそも最近のjQueryで動くんですかね...(検証してない) • (仮に動いても)今後は使わないほうがいい ◦ ここまでで紹介した手法で置き換えが可能
  16. CSSアニメーションの実行速度が影響してテスト実行結果が不安定になること を防止する CSSアニメーションを切る # in spec/rails_helper.rb Capybara.disable_animation = true Capybara::Server::AnimationDisabler

    でHTMLヘッダーにアニメーションを止めるスタイル指定を埋 め込んでいる。 この設定では不十分という場合は上記設定を使わず、自前でテスト実行時に使うアニメーション無効化す るためのCSSを用意してtest実行時だけそれを読み込むようにすれば良い。
  17. assets:precompileを先に走らせる # CI上で bin/rspec 実行前に走らせる RAILS_ENV=test bin/rails assets:precompile 最初のsystem specが実行される前にassets:precompileを走らせておき、

    初回に実行されるテストでのロード時間延長などによる不安定な結果やタイム アウトを防ぐ。 CI上でassets:precompileを実行するため、staging/production環境にデ プロイする前の動作確認にもなる。
  18. (オプション) config.assets.compileの無効化 # in config/environments/test.rb if ENV[‘CI’] config.assets.compile = false

    end 加えて、config.assets.compileを無効化することでassets:precompileし た後で参照できないassetsがあるとそのままテストが落ちてくれる。
  19. 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 を組み合わせたりと追加の手間がかかる。
  20. • CSSアニメーションを切る • assets:precompileは先に走らせておく • ブラウザコンソールのログをチェックする 不安定テストの防止・検出に役立つ設定 • findの挙動を知る •

    正しく要素をfindする • ユーザー目線で起こる状態変化をチェックする 落ちづらいテストを書かないためのポイント