Slide 1

Slide 1 text

2020/06/12 @銀座Rails #22 Masatoshi Iwasaki 不安定テストを生み出す Capybaraを調教する

Slide 2

Slide 2 text

Masatoshi Iwasaki 自己紹介 masa-iwasaki masa_iwasaki フリーランスエンジニア

Slide 3

Slide 3 text

RailsでCapybaraを使ったテストが不安定なときに 「とりあえずこれ試して!」 というTipsについてまとめました (「もうやっているよ!」ということも多いかも...) 今日のテーマ

Slide 4

Slide 4 text

● rspecを対象としてます ○ minitestでも役に立つことはあるかもしれません ● Capybara + SeleniumDriver前提 ○ webdrivers gem使ってるならこのパターン ○ headless chrome以外の経験が無いのでFirefoxとか他のブラウ ザだと違うこともあるかもしれません ● system spec前提 ○ feature spec使ってる場合は適宜読み替えてください 対象とする環境

Slide 5

Slide 5 text

● Capybaraが関与しないテストに関すること ○ model spec, request specなど ● 高速化 ○ 一部高速化に寄与するものもあります 話さないこと

Slide 6

Slide 6 text

● ここに書かれていることを全部実施したから不安定なテストが撲滅できる ということは、たぶんない 約束できないこと

Slide 7

Slide 7 text

(一応)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が それらを使うための便利メソッドを提供し てくれている

Slide 8

Slide 8 text

不安定テストとは?

Slide 9

Slide 9 text

ほんとは通る(greenになる)はずなのに、 なぜか原因不明で落ちる(redになる)テストのこと。 (本発表での)不安定テストの定義 つまり、テスト自体は正しく書けている(はず)前提。 稀に「ほんとは落ちるべきだったのにほとんどのケースで通っていた」という悲しい事実が発覚することもある

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

落ちづらいテストを書かない方法

Slide 13

Slide 13 text

1. findの挙動を知る 2. 正しく要素をfindする 3. ユーザー目線で起こる状態変化をチェックする 落ちづらいテストを書かないために

Slide 14

Slide 14 text

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 へと処 理が進む

Slide 15

Slide 15 text

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 個々の要素を 追っていくと結構複雑

Slide 16

Slide 16 text

簡略化 begin セレクタを探すクエリの実行 rescue StandardError => e raise e if 例外がElementNotFound以外 raise e if 制限時間超えている sleep(0.01) retry ... 大筋に関係ない処理を抜いてざっくりな挙動を抽出するとこんな感じ

Slide 17

Slide 17 text

findの挙動まとめ ● findは渡されたセレクタにマッチする要素を見つけるためのクエリを作る ● まずクエリを実行してElementNotFound以外の例外が来たらそのまま raiseする ● 例外がElementNotFoundで制限時間(後述)以内だったら0.01秒寝て から再度クエリを投げることを繰り返す ● 制限時間を超えていたら例外をraiseする ○ 最後の実行がそれまで通り終了したらElementNotFoundをraise することになる

Slide 18

Slide 18 text

1. findの挙動を知る 2. 正しく要素をfindする 3. ユーザー目線で起こる状態変化をチェックする 落ちづらいテストを書かないために

Slide 19

Slide 19 text

findの挙動まとめ(再掲) ● findは渡されたセレクタにマッチする要素を見つけるためのクエリを作る ● まずクエリを実行してElementNotFound以外の例外が来たらそのまま raiseする ● 例外がElementNotFoundで制限時間(後述)以内だったら0.01秒寝て から再度クエリを投げることを繰り返す ● 制限時間を超えていたら例外をraiseする ○ 最後の実行がそれまで通り終了したらElementNotFoundをraise することになる

Slide 20

Slide 20 text

findの隙を突かれるケース ● セレクタの指定が不十分 ● findと異なる待ち方をするfinderメソッド ● 制限時間を超える実行時間

Slide 21

Slide 21 text

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が要素が見つかるまで一定時間待ってくれる。

Slide 22

Slide 22 text

2)findと異なる待ち方をするfinderメソッド ● Capybara::Node::Finders#all ○ find_allはallのエイリアス ○ firstも内部でallを呼んでいる ● クエリ生成はfindと一緒だが、クエリ実行時にElementNotFoundを raiseすることはない ○ allの場合は何もマッチしないと空配列を返す ○ firstは1つも該当しなかったら例外を出すが、1つでも見つかったらそ れでOK ● 「all使ったから指定したセレクタの要素が全部返ってくる」という保証はな い

Slide 23

Slide 23 text

● Write Reliable, Asynchronous Integration Tests With Capybara ○ 2014年に書かれた記事(最終更新は2019年) ○ 先ほどのbad/goodのコードは本記事から引用 ● この記事ではfind等の待ち方に関する解説が薄く、紹介されている bad/goodの違いがわかりづらいのが難点だなと感じていた ○ 実はこれが今回発表しようと思ったきっかけ ● さすがに古くなっていて挙動が最新と違う点もあるが、bad/goodコードは 今でも使えるノウハウ ○ 不安定なテストに遭遇したらいつも読み直してます このあたりの元ネタ

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

1. findの挙動を知る 2. 正しく要素をfindする 3. ユーザー目線で起こる状態変化をチェックする 落ちづらいテストを書かないために

Slide 26

Slide 26 text

ブログ記事登録のsystem specで登録してからDBの中身をチェックするよう なケース 個人的な不安定テストの頻出例 # system specの一部 fill_in "タイトル", with: "CI不安定で切ない " click_on "登録" # 登録ボタンを押したときに非同期処理だと BlogEntryの追加が完了していない可能性があるが、 # 開発環境だと高速に処理が行われるので失敗することがほとんどない expect(BlogEntry.last.title).to eq "CI不安定で切ない " ※system specでActiveRecordインスタンスの値をチェックすることの是非に議論はあるかもしれない が、本発表では対象外とする。

Slide 27

Slide 27 text

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不安定で切ない"

Slide 28

Slide 28 text

ユーザ通知がすぐ消える(見えなくなる)ときも大丈夫? 疑問

Slide 29

Slide 29 text

findの挙動まとめ(また出たな!) ● findは渡されたセレクタにマッチする要素を見つけるためのクエリを作る ● まずクエリを実行してElementNotFound以外の例外が来たらそのまま raiseする ● ElementNotFoundで制限時間(後述)以内だったら0.01秒寝てから再 度クエリを投げることを繰り返す ● 制限時間を超えていたら例外をraiseする ○ 最後の実行がそれまで通り終了したらElementNotFoundをraise することになる ● 登録処理が非同期であればこの初回クエリ実行のほうが flashが表示される(サーバーサ イドの処理が終わる)より先に実行される可能性のほうが高い。 ● (待ち時間超えない限りは)flashが表示されるまでfindが待ってくれる。 ● 人間が認知するために必要な表示時間はコンピュータに取っては十分すぎる時間

Slide 30

Slide 30 text

● 同期・非同期にかかわらず、状態遷移の確認はユーザーに対して表示さ れる通知及び通知を含むDOM要素をチェックする ○ 現時点で同期的処理していても将来的に非同期処理になることもあ る ● 要所要所でチェックする習慣をつけることで、不安定テストが生じる可能 性を下げることができる。 ○ かつ、ユーザー目線でのsystem spec実装になってくる ユーザーに対する確認表示をひたすらチェック

Slide 31

Slide 31 text

● findしたい要素を一意に指定するにはセレクタの記述力が求められる ケースがある ● CSSセレクタを学ぶ(学び直す) ○ 知らなかった便利セレクタが実装されていたりするかも ○ caniuse.comで実装されているブラウザのバージョンを把握する必 要あり ● XPathも必要に応じて使う ○ xpathのほうが簡単なケースもあるため(親要素見つけるときとか) セレクタ力を高めて不安定テストを減らす

Slide 32

Slide 32 text

sleep() less ● ここまで紹介してきた事項を組み合わせることでsleep()は消せる ● 「そもそもsleep使わなくていいのが普通」のほうがよくて、頻繁に利用するも のではないという認識が良さそう ● 参考資料:ちょうどよいRails E2E test ○ アプローチによりsleep使ってないという実例 ○ 不安定テスト対策については言及されていないが、ユーザー目線での 実装についてはアプローチが共通していて、かつこちらのほうがより説 明が充実している

Slide 33

Slide 33 text

sleep()は再現手法として使う ● 待ち時間超過を手元で再現するのにsleepは有効 ○ サーバーサイドで sleep(3) とかdefault_max_wait_timeより大きい 数値を指定するだけ ● CIでこけたテストが「これ待ち時間切れたかなぁ」というときに手軽に検証で きる ○ 闇雲にsystem spec側で待ち時間を増やさなくて済む

Slide 34

Slide 34 text

wait_for_ajaxメソッドについて ● Automatically Wait for AJAX with Capybara で紹介されている手法 ○ 古くからあるRailsプロジェクトだと実装されているかも ● jQuery前提の実装 ○ かつ、jQueryをサーバーサイドとの通信に使っていなければ使えない ○ そもそも最近のjQueryで動くんですかね...(検証してない) ● (仮に動いても)今後は使わないほうがいい ○ ここまでで紹介した手法で置き換えが可能

Slide 35

Slide 35 text

Seleniumの詳細な動作を知りたい場合 Selenium::Webdriver::Loggerが環境変数のDEBUGを見て標準出力に詳 細ログを出してくれる(!?) https://github.com/SeleniumHQ/selenium/blob/6f36f8eff770eb3329a7ad49d9bc2c411f4983a5/rb/lib/selenium/webdriver/commo n/logger.rb#L143-L143 これは... DEBUG=true bin/rspec path/to/some_spec.rb

Slide 36

Slide 36 text

(おまけ) 不安定テスト対策の便利設定

Slide 37

Slide 37 text

特にCI環境ではやっておきたい。 ● CSSアニメーションを切る ● assets:precompileは先に走らせておく ● ブラウザコンソールのログをチェックする 不安定テストの防止・検出に役立つ設定

Slide 38

Slide 38 text

CSSアニメーションの実行速度が影響してテスト実行結果が不安定になること を防止する CSSアニメーションを切る # in spec/rails_helper.rb Capybara.disable_animation = true Capybara::Server::AnimationDisabler でHTMLヘッダーにアニメーションを止めるスタイル指定を埋 め込んでいる。 この設定では不十分という場合は上記設定を使わず、自前でテスト実行時に使うアニメーション無効化す るためのCSSを用意してtest実行時だけそれを読み込むようにすれば良い。

Slide 39

Slide 39 text

assets:precompileを先に走らせる # CI上で bin/rspec 実行前に走らせる RAILS_ENV=test bin/rails assets:precompile 最初のsystem specが実行される前にassets:precompileを走らせておき、 初回に実行されるテストでのロード時間延長などによる不安定な結果やタイム アウトを防ぐ。 CI上でassets:precompileを実行するため、staging/production環境にデ プロイする前の動作確認にもなる。

Slide 40

Slide 40 text

(オプション) config.assets.compileの無効化 # in config/environments/test.rb if ENV[‘CI’] config.assets.compile = false end 加えて、config.assets.compileを無効化することでassets:precompileし た後で参照できないassetsがあるとそのままテストが落ちてくれる。

Slide 41

Slide 41 text

● JSの実行時エラー・警告などが出ていないかをチェックすることで、JSが 理由でテストが失敗しているケースに気づきやすくする。 ● 最初にCapybaraで利用するdriverの設定が必要であるため、やや手間 がかかる。 ● capybara-chromedriver-logger を利用したり、このgemの中身を見て既 存の設定と上手く合わせるようにするとスムーズかもしれない。 ブラウザコンソールのログをチェックする

Slide 42

Slide 42 text

READMEにある画像(以下)のようにブラウザのコンソールに表示されるログ が出る。 capybara-chromedriver-logger ここからcapybara-chromedriver-logger を使った場合のtipsを紹介。使わ ない場合でもログ出力する場合は共通する話のはず。

Slide 43

Slide 43 text

ログのノイズを減らす Vue.config.devtools = false; Vue.config.productionTip = false; 運用上、通常は一切ログがでないことを前提として、ログが出たらすべてfailさ せるようにしないと、CIの実行時ログを追わないといけないため、気づきづら い。 たとえばVue.jsだとDevToolsやproduction向けのtipsなどが表示されるの で、assets:precompileの時点などで表示しないようにしておきたい。

Slide 44

Slide 44 text

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 を組み合わせたりと追加の手間がかかる。

Slide 45

Slide 45 text

本日のまとめ

Slide 46

Slide 46 text

● CSSアニメーションを切る ● assets:precompileは先に走らせておく ● ブラウザコンソールのログをチェックする 不安定テストの防止・検出に役立つ設定 ● findの挙動を知る ● 正しく要素をfindする ● ユーザー目線で起こる状態変化をチェックする 落ちづらいテストを書かないためのポイント

Slide 47

Slide 47 text

最後に

Slide 48

Slide 48 text

本日のタイトル

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Capybara全然悪くない!!

Slide 51

Slide 51 text

調教が必要なのは我々だった https://www.flickr.com/photos/89654772@N05/8157292018 加工済み

Slide 52

Slide 52 text

ご清聴ありがとうございました