2021年6月現在、Cupriteで"正しい"システムテストはできるのか?
まえおき
Railsでsystem specを導入するぞ! というときに、Google検索をすると
単に「Cuprite使えばいいよ!」ではなく、そもそものシステムテストの必要性や開発経緯なども親切に書かれており、非常に素晴らしい記事です。
ただ、結果的にCupriteを称賛した内容で終わっており、人によっては「Seleniumなんか使わずにCuprite使えばいいんだ!」って思う人もいるかも知れません。
個人的にPlaywrightベースのCapybaraドライバを開発していて、その開発の際にCapybaraのソースとかCupriteの動作とか結構見たので、知見の共有をしておこうかなと思います。
SeleniumベースのCapybaraドライバ がスタンダード。それでも、Yet Another Capybaraドライバを求める理由はなんだっけ?
そもそもなぜ別のドライバを使わないといけないのか?というところです。
いろいろ理由はありそうですが、だいたい次の2つのどちらかに落ち着きます。
- Seleniumに対する不満
- Capybara自体の機能不足を補うため
細かく挙げていきます。
Seleniumはセットアップが面倒
この記事でも言及されていました。
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は遅い??
これも以下の記事で言及されていました。
でも、Seleniumってそんなに遅いですかね・・・? Cupriteってそんなに速いですかね・・・?
そもそも、ただ単純に速ければいいというものでもなく、DOM要素の変化通知をしっかりつかんでいなければ、速く動かしても不安定なテストが量産されるだけです。
そのため、かりにSeleniumが遅くてCupriteが速かったとしても、それ単体ではあまり重要な因子にはなりません。
SeleniumはDOM要素の変化通知に弱い
Seleniumが遅いと思われる理由の一つに、DOM要素の変化通知の取りこぼし、という要因が大きいのではないかと私は考えています。
「なぜか、ときどきDOMの変化をとりこぼしてしまう」ので、ゆっくり動かさないといけなかったり、不安定な部分にはリトライするように実装したり。そういう細かいスリープ/リトライ時間が積もり積もって"遅い"という体感・結果に至っているのではないかと。
Seleniumには「○○の要素が現れるまで待機する」、ということを実現するWaitっていうクラスがあります。
なので、精度が出ないのをSeleniumのせいにしちゃいけません。
救世主はMutationObserver
少し横道にそれるんですが、かりにSeleniumベースのCapybaraドライバがWaitクラスを使っていてそれが精度不足の原因だったとしましょう。
その場合には、PuppeteerやPlaywrightなどで使われているMutationObserverベースでDOMの変化監視が精度向上に役立ちます。これはDOMに変化があったらコールバックするというズバリの機能なので、「変化まだかなまだかな」とポーリングする実装に比べて取りこぼしのリスクを軽減できます。
ただ、ApparitionやCupriteなどのドライバは、MutationObserverを使うDOM監視の方法は提供していません。
つまり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 という基本操作がとても不安定な不具合があります。しかし、全然直される気配がありません。
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などを追加する必要があります。
実際にSeleniumからCupriteの移行を試してみて、かなり躓いた方もいるようです。
Cupriteは「Cupriteで最初から作り、今後もCupriteしか基本的には使わない」くらいの固い意志で選定する必要がありそうです。
結論
2021年6月現在、Cupriteで"正しい"システムテストは多分できます。
が、正しいテストができるかどうかよりも、「SeleniumではなくCupriteを選定する!」という固い意志決定を組織として行うことが重要そうです。
Discussion