2021年6月現在、Cupriteで"正しい"システムテストはできるのか?

7 min read読了の目安(約6400字

まえおき

Railsでsystem specを導入するぞ! というときに、Google検索をすると

https://techracho.bpsinc.jp/hachi8833/2020_08_06/95282
こういう記事が引っかかるかと思います。
単に「Cuprite使えばいいよ!」ではなく、そもそものシステムテストの必要性や開発経緯なども親切に書かれており、非常に素晴らしい記事です。

ただ、結果的にCupriteを称賛した内容で終わっており、人によっては「Seleniumなんか使わずにCuprite使えばいいんだ!」って思う人もいるかも知れません。

個人的にPlaywrightベースのCapybaraドライバを開発していて、その開発の際にCapybaraのソースとかCupriteの動作とか結構見たので、知見の共有をしておこうかなと思います。

SeleniumベースのCapybaraドライバ がスタンダード。それでも、Yet Another Capybaraドライバを求める理由はなんだっけ?

そもそもなぜ別のドライバを使わないといけないのか?というところです。
いろいろ理由はありそうですが、だいたい次の2つのどちらかに落ち着きます。

  • Seleniumに対する不満
  • Capybara自体の機能不足を補うため

細かく挙げていきます。

Seleniumはセットアップが面倒

https://techracho.bpsinc.jp/hachi8833/2020_08_06/95282

この記事でも言及されていました。
Capybara関係なく、Selenium経由でブラウザを操作するには

  • selenium-webdriver
  • chromedriverやedgedriverなどのWebドライバ

が必要です。

DockerでSelenium Hubなどを使えば比較的セットアップは楽ですが、ケイパビリティだのなんだの、設定項目が(初見だと)わりと謎いです。

自動操作したい対象がChromeだけでいいとすると、Chrome DevTools Protocolあたりを使って自動操作する方法もあるので、「CDPでいいやん?」となるのは自然な発想です。

  • ApparitionはCDPを使ってChromeを直接操作するCapybaraドライバです
  • Cupriteは、CDPを使ってChromeを操作するFerrumというライブラリを使ってブラウザ操作するCapybaraドライバです

Seleniumは遅い??

これも以下の記事で言及されていました。

https://techracho.bpsinc.jp/hachi8833/2020_08_06/95282

でも、Seleniumってそんなに遅いですかね・・・? Cupriteってそんなに速いですかね・・・?

そもそも、ただ単純に速ければいいというものでもなく、DOM要素の変化通知をしっかりつかんでいなければ、速く動かしても不安定なテストが量産されるだけです。
そのため、かりにSeleniumが遅くてCupriteが速かったとしても、それ単体ではあまり重要な因子にはなりません。

SeleniumはDOM要素の変化通知に弱い

Seleniumが遅いと思われる理由の一つに、DOM要素の変化通知の取りこぼし、という要因が大きいのではないかと私は考えています。

「なぜか、ときどきDOMの変化をとりこぼしてしまう」ので、ゆっくり動かさないといけなかったり、不安定な部分にはリトライするように実装したり。そういう細かいスリープ/リトライ時間が積もり積もって"遅い"という体感・結果に至っているのではないかと。

Seleniumには「○○の要素が現れるまで待機する」、ということを実現するWaitっていうクラスがあります。

https://www.selenium.dev/documentation/ja/webdriver/waits/
ただ、実はSeleniumベースのCapybaraドライバはWaitクラスを利用していません。Capybaraは内部で自前で「要素が現れるのまだかなまだかなー」ってポーリングしています。
なので、精度が出ないのをSeleniumのせいにしちゃいけません

救世主はMutationObserver

少し横道にそれるんですが、かりにSeleniumベースのCapybaraドライバがWaitクラスを使っていてそれが精度不足の原因だったとしましょう。

その場合には、PuppeteerやPlaywrightなどで使われているMutationObserverベースでDOMの変化監視が精度向上に役立ちます。これはDOMに変化があったらコールバックするというズバリの機能なので、「変化まだかなまだかな」とポーリングする実装に比べて取りこぼしのリスクを軽減できます。

ただ、ApparitionやCupriteなどのドライバは、MutationObserverを使うDOM監視の方法は提供していません

https://github.com/rubycdp/ferrum/issues/82

つまりDOM要素の変化監視に関しては、SeleniumベースのCapybaraドライバと同程度かそれ以下の精度しか期待できないのです。

Capybaraの自動操作の精度がよくない

「なぜか時々、そこにあるはずの要素をクリックしてくれない」みたいな問題はだいたいこれです。

Selenium単体で使ってもそういうことはありますが、SeleniumベースのCapybaraドライバ
は前述の通りSeleniumのWaitクラスを使用していませんので、精度が出ない原因はたいていCapybaraのDOM変化監視ロジックによるものです。

      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

ソースはここ

clickとかもろもろの自動操作系は、内部で上記のsynchronizedメソッドを利用していて、だいたい以下のように動いています。

  • (デフォルト2秒, default_max_wait_timeで指定された秒数の)タイマーを仕掛ける
  • blockで指定されたお仕事をする
  • 要素が見つからない場合には、0.01秒スリープして、リトライする
  • もしタイマーが切れたらあきらめる

このロジックはCapybara本体に含まれるため、ドライバには依存しません。
SeleniumベースのCapybaraドライバでも、ApparitionでもCupriteでも、このロジックは共通です。
どんなドライバを使ったとしても、 sleep 0.01 を含むポーリングによるDOM変更監視をしているので、Capybara DSLを使う限りは精度は出ません。

「CupriteはCDPベースなので、きっと精度が出るはず...!」という淡い期待を寄せて使うと、裏切られてしまいます。

Capybara DSLではできないことをやりたい

ApparitionもCupriteも、Capybara DSLではできない機能をそれぞれ提供しています。

とくに、Cupriteでは Capybara.current_session.driver.browser でFerrumのBrowserインスタンスを直接参照できるので、Ferrumのexampleにあることは全部できます

ただ、逆にいうと、Ferrum::Browserにどっぷり依存した自動操作スクリプトを書いてしまうと、Cuprite以外のCapybaraドライバへの移行は難しくなります。自分がやりたいことはFerrumやCupriteでできるのかを見極めた上でトレードオフを理解して使うのが大事です。

Cupriteを選定する理由

SeleniumベースのCapybaraドライバを選ばない 理由をなんとなくいろいろ書いてきましたが、ではCupriteを選定する理由はどういうものがあるでしょうか。

Pure Ruby implementationなので、導入がカンタン

https://techracho.bpsinc.jp/hachi8833/2020_08_06/95282 でも言及されています。

自動操作ライブラリのFerrumがベースなので、品質が比較的安定している

Apparitionも同じくPure Ruby implementationですが、ApparitionはCapybaraドライバの中にCDPを利用したブラウザ操作のコードが直接入っています。いっぽう、CupriteはCapybaraドライバとしてのインターフェース+αに特化しており、ブラウザを自動でぐりぐり動かす責務はFerrumが専属で担っています。

Capybaraは自動テスト用途でしか使われないため、スクレイピング目的のユーザはFerrumを直接利用します。Appritionではそれができませんので、ブラウザ自動操作の部分の品質が結果的にCuprite/Ferrumよりも低くなってしまいます。

現に、Apparitionには click という基本操作がとても不安定な不具合があります。しかし、全然直される気配がありません。

https://github.com/twalpole/apparition/issues/76

Ferrumだとこのレベルの不具合はスクレイピングユーザによって気づかれるので、放置される可能性は低そうです。(結局は開発チーム次第なので、確実には言えませんが...)

Cupriteを選定する上で気をつけたい点

これまで書いてきたことの繰り返しになってしまう部分もありますが、一応まとめておきます。

SeleniumベースのドライバをCupriteにするだけで、テストが速くなるわけではない。

前述のとおり、DOMの変更検知の能力はSeleniumとCupriteで変わりませんので、かりにテストをスピードアップさせようとしてもただ不安定になるだけです。

Chromeの起動速度が若干だけ速くなる、とかそのレベルの期待にとどめたほうがよさそうです。

Seleniumで「なぜか時々落ちる」テストは、Cupriteにしても直らない

前述の通り、Capybaraの共通ロジックである0.01秒スリープ&リトライはドライバによらず同じですし、CupriteはPuppeteerやPlaywrightのような独自のDOM変更検知メソッドを提供していませんので、DOM変更検知の能力は変わりません。Seleniumで不安定なテストはCupriteにしても不安定なままです。

個人的にここに大きな課題を感じていて、Playwrightネイティブの強力なDOM変化通知が使えるcapybara-playwright-driverっていうのを開発しました(しれっと宣伝w)

SeleniumベースのCapybaraドライバとCupriteは微妙に動作に互換性がない

CupriteはCapybaraの共通specは実装していますが、一部SeleniumベースのCapybaraドライバとは動作が異なる部分があります。

例えば、 fill_in の際に、SeleniumベースのCapybaraドライバではテキストボックスにフォーカスがあたりますが、Cupriteではフォーカスが当たりません。そのため明示的にclickなどを追加する必要があります。

https://github.com/rubycdp/cuprite/issues/157

実際にSeleniumからCupriteの移行を試してみて、かなり躓いた方もいるようです。

https://patorash.hatenablog.com/entry/2021/06/04/105556

Cupriteは「Cupriteで最初から作り、今後もCupriteしか基本的には使わない」くらいの固い意志で選定する必要がありそうです。

結論

2021年6月現在、Cupriteで"正しい"システムテストは多分できます。

が、正しいテストができるかどうかよりも、「SeleniumではなくCupriteを選定する!」という固い意志決定を組織として行うことが重要そうです。