フレーキーテストの解消に役立つ便利な gem capybara-lockstep

この記事は[ソニックガーデン プログラマ アドベントカレンダー](https://qiita.com/advent-calendar/2024/sonicgarden)の16日目の記事です。

---

# はじめに
Rails のシステムテストって便利ですよね。実際にユーザーが使うシチュエーションをブラウザからRails、データベースまで包括的にテストできる。素晴らしい仕組みだと思ってます。
そんなシステムテストですが、アプリが大きく成長してきてユーザー体験を重視したリッチなUIやUXが充実しだしたりしたときに、こんな状態に遭遇しだしたりします。

* なぜか、CIで自動テストが通らない
* 再実行したらうまくいく
* ローカルでは、テストが通る（テストの失敗が再現しない）

実際に体験した人ならわかると思うんですが、CIと同じ状況をなかなか再現できなくって厄介ですよね。
原因のわからない不可解な事情にプログラマとしては、どうにかしたいなと思いつつ、泣く泣く再実行してテストが通るまでリトライボタンを押したり、ローカルではテストが通る旨をコメントしたりしてました :sob:

このような通ったり、失敗したりするという不安定なテストは、巷ではフレーキーテストと呼ばれています。
今回はそんなフレーキーテストの解消に役立つ便利な gem [capybara-lockstep](https://github.com/makandra/capybara-lockstep) を紹介します。

# なぜ、フレーキー？

システムテストは、実際にユーザーが行っているクリックなどのブラウザの操作を **高速** で実行しています。その操作は我々が実際にブラウザで行う操作よりも断然速く、
* 投稿処理中に別のページに遷移する
* css などが読み込まれるよりも前に処理する

などといった、人間では実現できないようなレベルでの操作も可能になっています。
このような状況下では、本来だったらユーザー体験に違いがでないであろう、

* テスト実行時のサーバーの実行速度
* ブラウザの実行速度
* マシンのスペック

といった要素によって動作が変わるということが発生し、これがフレーキーさへとつながっていたりします。

では、このような状況はどのように対処すればいいでしょう？

原因としては、**なにかが処理中に次の処理が実行されていること** であるため、その対処法としては、処理が完了したことを待機するというアプローチが効果的になります。
例としては、以下のようなコードです。

```ruby
click_on '投稿する'
expect(page).to have_content('投稿しました') # 投稿後に表示される要素があることを確認することで、処理完了まで待機させている
click_on '一覧へ'
```

対処方法としては、このような実行処理の完了を待つコードを随所に書くことで解消はできます。
ですが、それなりに大きく育ったアプリで突如不安定になりだしたときでは、随所に一つ一つに完了を待つコードを組み込むのは、なかなか骨の折れる作業です。。。

そこでそのような完了を待つ処理をお手軽に実現する方法として紹介するのが、今回紹介する gem [capybara-lockstep](https://github.com/makandra/capybara-lockstep) です。

# capybara-lockstep

この gem を導入すると、

* Capybaraが要素を検索する
* Capybaraユーザー・インタラクション（クリック、タイプなど）をシミュレートする
* Capybaraが新しいURLにアクセスする
* CapybaraがJavaScriptを実行する

のような操作を行うときに、

* すべてのドキュメントリソース（画像、CSS、フォント、フレーム）が読み込まれるのを待ちます
* クライアント側JavaScriptがDOM要素をレンダリングまたはハイドレートするのを待ちます
* 保留中のAJAXリクエストが終了し、それらのコールバックが呼び出されるのを待ちます
* 動的に挿入された`<script>`が読み込まれるのを待ちます（動的インポートやAnalyticsスニペットなど）
* 動的に挿入された`<img>`または`<iframe>`要素が読み込まれるのを待ちます（遅延ロードされた要素は無視されます）

ということをしてくれるようになります。
Capybara が次の操作を行うときに、css や ajax などの処理の実行を待ってくれる。つまり、lockstep してくれるわけです。


## 導入方法
導入に必要なコードは以下、3箇所だけです！

#### 1) capybara_lockstep helper を html に埋め込む
```erb
## app/views/layouts/application.html.erb
<%= capybara_lockstep if defined?(Capybara::Lockstep) %>
```

#### 2) Capybara::Lockstep::Middleware を組み込む
```rb
### config/environments/test.rb
config.middleware.insert_before 0, Capybara::Lockstep::Middleware
```

#### 3) Selenium WebDriver の unhandled_prompt_behavior を設定
```rb
Capybara.register_driver(:selenium) do |app|
  options = Selenium::WebDriver::Chrome::Options.new(
    unhandled_prompt_behavior: 'ignore',
    # ...
  )
  Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end
```

これだけで、かなり安定したテストが実現できるようになります。

実際に筆者も、CIでだけテストが落ちる、ローカルではテストが落ちないと不安定だったテストがあって困っていたのですが、この gem を導入することで、ローカルでも同じ状態が再現できるようになり、その原因の究明にかなり役立ちました。
なお原因の究明する際には、`Capybara::Lockstep.debug` がオススメです。
これを使うと、サーバー、クライアント間の動作状況がログに残ってくれるので、便利です。
かなりの量のログが流れてくるので、以下のように環境変数で動作を切り替えられるようにしておくとデバッグが捗ります。

```rb
Capybara::Lockstep.debug = true if ENV['LOCKSTEP_DEBUG']
```

## 導入結果

実際に導入してみての結果ですが、筆者の環境ではCIでしか再現しないテストに困っていたが、ローカルで再現することができました。
一方で、仕組み上どうしても導入前よりも強制的な待機時間が増えるので、 テストの実行時間が気になると思うので、導入前後で実行時間を比較してみましょう。

比較としては、以下のようになりました。

![スクリーンショット 2024-12-11 10.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48853/b7a909a7-c0e8-8534-a78a-3fe0f3092b1a.png)

それなりに育ったプロジェクトなので、テスト並列に実行したりしていて、実行時間が単純なテストだけではないので、これがすべてテストによる影響の増分とはいえないのですが、48m13s ⇛ 56m33s なので +17% ぐらいでした。

どうでしょう？
やはり実行時間は伸びてしまう傾向にはあるのですが、個人的には、従来であれば再実行させたりする時間と手間があったことを考えれば十分許容範囲とも言える範囲かなーと思います。

# まとめ

今回は、フレーキーテストの解消に役立つ便利な gem として [capybara-lockstep](https://github.com/makandra/capybara-lockstep) を紹介しました。

lockstep することで、サーバー、クライアント間の不安定さがなくなり、テストの再現性が高くすることができます。
これにより1テストの実行時間は伸びるかもしれないですが、リトライの必要がなくなるので、その分全体としての処理時間は改善につながるかもしれないので、テストでハマって困っている人はぜひ、検討してもらえればと思います。

---

[ソニックガーデン プログラマ アドベントカレンダー](https://qiita.com/advent-calendar/2024/sonicgarden) 17日目は [@jnchito](https://qiita.com/jnchito "jnchito") です。お楽しみに！