ja ja ja de ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en en en ja ja ja ja ja ja ja ja ja ja en ja ja ja en ja en fr ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja en nl ja en ja ja ru ja ja nb ja ja en ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja cv es es ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
docker環境だと↑はできませんが、chromeのremote debugging機能を使うことで同等のことができます。やり方はRails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記 が参考になります
CIでたまに失敗するテストであれば、ローカルで再現するか試してみる
例えば次のように書いて rspec --fail-fast ファイル名:行番号 とすると1000回テストを実行して最初に失敗したところで止まります。これが全部通るのであれば、順番依存などで失敗するテストの可能性が高いです。
1000.times do it do # たまに失敗するテスト end end
テストがどのように動いているかを学ぼう
ログやスクリーンショットを見てもよくわからん、となるとき、そもそもテストがどう動いているかについてよく知らないのであたりがつけられない可能性があります。テストはあくまで本番を模して確認するためのもので、本番とは違ったテクニックで実現されています。それらのテクニックを把握してると、失敗の原因を推測できる可能性が高まります(もちろん本番環境がどのように動いているのか知るのも大事です)。
以下抑えておいてほしいテストの仕組みについて書いています。
テストは基本的に次の3つのファイルで設定されているはずです。特に「開発環境だとちゃんと動いているように見えるんだけどテストだとなぜかうまくいかない」ようなケースではtest.rbとdevelopment.rbをみて、どのような差分があるかチェックしてみるとよさそうです。
config/environments/test.rb
spec/rails_helper.rb
spec/spec_helper.rb
例えばActive Jobは各環境で別々のアダプタを使っている事が多いです。本番時にはsidekiqを利用するけれど、開発時にはasyncアダプタ(スレッドを利用した非同期実行)を利用し、テスト時にはtestアダプタ(テスト用のキューにジョブの情報を入れるだけでジョブ自体は実行しない)を選択するような設定が一般的です。
このように、テストを実行するために本番や開発時とは違う形にしている箇所はいくつかあるはず。思わぬ箇所の設定が影響していることがあるので、設定のそれぞれの行がなにを表しているのか説明できるようになっているのが望ましいです(大変だけど)。
各種設定の内容はRailsガイドにまとまっています。読んだけどよくわからない、という場合はお近くの技術顧問に質問ください。
テスト中に行った変更は必ずもとに戻そう
複数のテストを実行するときは、それぞれ独立した環境でテストを実行したいところです。例えばあるテスト中にレコードをinsertしたら、テスト終了時に何らかの形でDBをロールバックしてから次のテストを実行しないとテストが不安定になってしまいます。
RailsはデフォルトでDBのトランザクションを利用してロールバックする仕組みを持っています。最近のRailsプロジェクトだとすべてその方式(トランザクションを利用してロールバックする)で統一できますが、昔のRailsプロジェクトだとfeature spec用にdatabase_cleanerやdatabase_rewinderを利用してDBを毎回空にして対応しているケースもあります(詳細はコラム参照のこと)。
つまり、DBに関しては特に意識しなくても自動で元の状態に戻るようなっているはずです。またrspec-mocksによるモックの設定などもテスト終了時に自動でもとに戻してくれています。べんりですね。
しかし、ツール側で対応していないものは手動で元に戻さないといけません。例えば、csvファイルを出力するメソッドをテストする場合、作ったcsvファイルをテスト終了時に削除しないとファイルがずっと残ってしまいます。そのcsvファイルの有無で挙動が変わるようなアプリケーションコードがあったら、別のテストが失敗してしまうかもしれませんね。
このように、テスト中の変更をもとに戻さないと順番依存で失敗するテストが出来上がるので、意識して変更をもとに戻すようにしましょう。順番依存で失敗するテストは再現がなかなか難しいです。機械的に順番依存失敗テストを再現させる方法としてはrspec の --bisectオプションがあります(ローカルで大量のテストを--bisectするのは骨が折れるので、実際に適用できるケースは少なそうですが)。
参考: Rails tips: ランダムにコケるRSpecテストの修正に便利なbisect(翻訳)
[コラム] feature specと system specの違い
feature specでは、テストコード用のスレッドとテストサーバのスレッドが異なります。結果としてDBのコネクションが別々になり、テストコード用のスレッドでトランザクションを利用するとテストサーバ側のスレッドはそれを参照できません。つまりトランザクションによるロールバックの仕組みを利用できないことになります。なのでfeature specではそれぞれの変更を都度COMMITし、テスト終了時にTRUNCATE文やDELETE文でレコードを削除するのが通例でした。それを自動で行うためにdatabase_cleanerやdatabase_rewinderのgemが使われていました。Rails 5.1から利用できるsystem specではこの仕組をハックして各スレッド間で同一コネクションを使えるようにして、E2Eのテストでもトランザクションを利用できるようにしています。そのため、system specではdatabase_cleanerやdatabase_rewinderは必須ではなくなっています。
E2Eテストの仕組みを知ろう
E2Eのテストは実際にブラウザを動かしてテストするため他のテストよりも複雑で、デバッグも大変なことが多いです。まず、昨今の一般的なRailsのE2Eテストではheadless chromeを利用することが多いと思いますが、その際には次の3つのプロセスが起動します(アプリケーションサーバにpumaを使用していると仮定)。
capybara(selenium driver)とpuma
※ capybara(selenium driver)とpumaは同一プロセスだが別スレッドで動いている
chrome driver
capybara がchrome driverのAPIを叩き、chrome driver が chrome のAPIを叩き、chromeはpumaにアクセスするという流れ。
それぞれが独立したプロセスで動いているので、テストを実行している最中に前のテスト関連のリクエストがpumaに届く、というケースがあります。ajaxで非同期にリクエストを投げる画面で起こりやすいです。これが原因でテストが時々落ちることがあります。
たとえばdeviseを利用しているプロジェクトでrequest specを実行するときに、deviseが提供するsign_inというヘルパメソッドを利用してログイン済み状態のリクエストを作ることがあります。このsign_inメソッドは、「次のリクエストをログイン済み状態として扱う」という挙動をします。これと先ほど説明した「テスト中に前のテストのリクエストが届く」を組み合わせるとたまに失敗するテストの一丁上がりです。前のテストのリクエストでsign_inメソッドによるログイン済み状態が解除され、request specが投げるリクエストが未ログイン状態になりテストが失敗します。
[コラム] chrome driverとchromeのバージョンを合わせる
chrome driverはバージョンごとにサポートしているchromeバージョンが違うので、ここが一致していないとE2Eのテストが全部失敗したりします。webdrivers gemを利用するとテスト実行時に自動でインストール済みのchromeに合ったバージョンのchrome driverをダウンロードしてくれるので積極的に使いましょう。
ただしwebdriversは並列テスト時に同時にchrome driverをダウンロードしようとして挙動がおかしくなることがあります。そのようなケースでは並列テストを実行する前に明示的にWebdrivers::Chromedriver.updateを実行してchrome driverをダウンロードすると回避できます。
capybaraの要素が現れる(or 消える)のを自動で待ってくれるのを知ろう
E2Eテストで問題になるのはajaxを利用している箇所とアニメーションを利用している箇所が多いのではないかと思います。
何かをクリックしたときにJavaScriptによって新しい要素が表示され、その新しい要素をクリックしたい場合は通常と変わらずclick_on '新しい要素'のように書きます。このときAPIを実行するなどでJavaScript中の処理が遅くclick_on実行時に新しい要素が表示されていないような場合、capybaraは一定時間待って再度要素を探す、ということを繰り返します。最大でCapybara.default_max_wait_time秒待ちます。デフォルトでは2秒ですが、5秒などにしているプロジェクトも多いのではないでしょうか。
これだけみるとJavaScript(ajax)を利用した画面でもなにも考えずにテストできるように思えますが、そんなことはありません。明示的に、何かが起きるのを待つためのコードを挿入しないとテストが失敗するケースがあります。
例えばモーダルダイアログがリンクの上に表示されているようなケースで「モーダルを閉じるボタンをクリックする→リンクをクリックする」という処理を書いたとします。このときはモーダルを閉じるときにすでにリンク自体は存在するので、先程書いた「要素が現れるのを待つ」は実行されません。なのでモーダルを閉じるのに時間がかかると、次のリンククリックに失敗します(上にモーダルが残っているのでクリックできない)。そのようなケースでは、リンクをクリックする前にモーダルが閉じられていることを明示的に確認し、もしモーダルが閉じられていなかったら待つようにするとうまくいくでしょう。例えば page.has_no_css?('.modal') のように書くとそれができます。
関連: capybaraの公式ドキュメント
capybara(selenium)によるクリックの仕組みを知ろう
capybaraのクリックの挙動も知っておく必要があります。capybaraは要素をクリックするときに
取得した座標をクリックする
という手順をとります。このとき1と2の間にタイムラグがあるため、そのときに
cssアニメーションの途中である
画像ローディングの途中である
などの理由により要素が移動するとクリックに失敗します。
cssアニメーションをオフにする機能はcapybaraが用意してくれています(Capybara.disable_animation = true)。これはcssアニメーションをオフにするstyleタグをRackミドルウェアを利用してすべてのページに挿入することで実現しています。ただし、あくまでこのようなscriptやstylesheetがテスト中のHTML中に差し込まれるだけなので、ここでオフにしている以外の方法でアニメーションを実現していたら別の方法が必要です。
画像のローディングに関しては特に公式のサポートはないので、↓のようなヘルパメソッドを定義して必要なところで呼び出してあげるとよいでしょう。
def wait_for_image_loading Timeout.timeout(Capybara.default_max_wait_time) do sleep 0.5 until evaluate_script(  e.complete ) JS end end
ここまでテスト固有のTipsについて書いてきました。
しかしRailsアプリケーションのデバッグに必要な知識はこれにとどまりません。Rubyそのものに対する知識も必要ですし、Railsを始めとしたgemのソースコードを読み解かなければ解決できない問題に遭遇することもあるでしょう。他にもプロセス、スレッド、ソケットやJavaScriptなど、Railsアプリケーションが関わっている技術の知識も欲しいところです。
範囲が膨大で大変ですが、近道はないので少しずつ勉強してきましょう。僕もがんばります。
willnet 2020-12-25 09:00
ActionCableでフィーチャスペックを書く
最近自分の作っているサービスにActionCableを導入しました。そ…
RSpec 3 時代の設定ファイル rails_helper.rb について
rspec-rails、3.0.1 がリリースされていますね。インストールし…
パーフェクト Ruby on Rails のサンプルアプリケーションを Github 上で公開しまし…
パーフェクトRuby on Rails 中の第6章で取り扱っている題材、イ…
capybara-webkit と font-awesome を仲良くさせる方法
皆さん、font-awesome は使っていますか?font-awesomeは、twit…
minitest と capybara で rails3 の integration テストを書く
ハマったのでメモ。素の minitest と capybara でテストを書く…
« TurbolinksからTurboへの移行 APIに利用制限をかけるとしたらどういうや… »
id:willnet はてなブログPro
べんりで楽しいwebサービスをたくさん作りたい
読者です 読者をやめる 読者になる 読者になる
@netwillnetをフォロー
失敗したテストのログだけを出力するぞ
iCARE Dev Meetup #34で登壇しました
`Thread.current[:hoge]`はスレッドローカル変数を参照していると思いきや実際はファイバーローカル変数だった