PlaywrightをRailsのsystem specから使いたくてCapybaraドライバを作った

気づけばかれこれ1年以上、Ruby言語向けのブラウザ自動操作のソフト開発を趣味の時間でやっている。

puppeteer-rubyで既存のRailsアプリケーションのシステムテストを段階的に良くしたい、というコンセプトは以前書いたが、最近はPlaywrightをRubyから使えるようにすることに注力している。

なので、playwright-ruby-clientを作っているから、puppeteer-rubyはもうオワコン?とはならない。用途が全然違う。

playwright-ruby-clientはどちらかというと、Ferrumに近い位置づけで、今後はCupriteのようなCapybaraドライバを作ったり、Vesselのようなクローラー書く仕組みを作ったり、みたいな方向性で成長させていく必要がある。

puppeteer-rubyはあくまでCapybaraとの「共存」ができることを強みに、(本家Puppeteerがなくならない限りはw)育てていくだろう。

(https://yusukeiwaki.hatenablog.com/entry/2021/01/24/puppeteer-ruby-on-fukuoka-ruby-award-13 の末尾から引用)

ここで書いてた「Capybaraドライバを作ったり」のところをまさに最近やっている。

(おさらい)Railsのsystem spec

一言でいうと、「FactoryBotでテストデータを作った状態&モックが効いた状態で、JavaScriptをふんだんに使ったページを自動試験する」という曲芸ができる優れもの。

人の手で再現困難な「外部のAPIサーバーが壊れていたときの○○の表示」とか「絶妙なタイミングで、二重決済が走ってしまったときの△△表示」みたいなのを機械的に状況を作って試験できる。

かなり夢が広がる仕組みなのだけど、実際に運用をすると1つだけ難点がある。それが、自動操作が不安定だということ。「なんかわからんけど20回に1回くらいコケる」みたいなことが稀によくある。

すでに試験を作ってしまった状況を救うpuppeteer-ruby

yusukeiwaki.hatenablog.com

ここに書いていたことそのものだが、発想としては 「CapybaraからSeleniumドライバを利用して ヘッドレスChromeを操作する」というベースの仕組みは変えず、Seleniumドライバが起動したChromeをPuppeteer経由でも操作できるようにして、Seleniumの弱みを補強する というものだ。

これから試験を作るなら...? → capybara-playwright-driver

もし既存の試験というものが無く、これからsystem specを書き始めるのであれば、どうせなら ChromeだけじゃなくてFirefoxWebKitも操作できるほうがありがたいと思うことも多いはず。

こんなつぶやきをしていたのだけど、IE11が本格的に滅び始めているので、もうSeleniumを使う意味ってほとんど無くて、Playwright使いたいよね、と。

ただ、RailsからPlaywrightでの自動操作をするには少しハードルが高い。

Playwrightは(Chromeを除いて)自分が起動したブラウザじゃないと自動操作できない ので、CapybaraがSeleniumドライバ経由でブラウザを起動しているところをごっそり置き換えてあげる必要がある。

途中のあれこれはさておき、とりあえずCapybaraのカスタムドライバを作ったのが capybara-playwright-driverだ。

github.com

Capybaraドライバの実装は本当に悪戦苦闘

ドキュメントはありません。ソースを見ましょう

そもそもCapybaraは利用者向けのドキュメントはそこそこあるが、ドライバ開発者向けの情報が皆無だ。既存のドライバのソースを斜め読みして、既存のドライバの挙動をサンプルコードで確認しつつ、zennにいろいろ書き残しながらやった。

zenn.dev

Capybaraが用意している単体試験は、実行順序に依存している?!?!

Capybaraには Capybara::SpecHelper というコンポーネントがあって、ここに各ドライバが満たすべき単体試験がたくさんある。(詳しくはこのへんに書いた→ https://zenn.dev/yusukeiwaki/scraps/280aabf289ae29#comment-ece446f4bd072c) 

単体試験の実装工数が大幅削減できるのは非常〜〜〜にありがたいのだけど、ただこいつは本当にくせもので、単発で実行したときと全体で実行したときで、なぜか結果が変わることがある。Capybaraはクラス変数にいろいろ詰め込んでいて副作用満載なので、なにかリセット処理がどこかで漏れているのだろう...。

単体試験の実行に時間がかかる

Firefoxの結果まで待つと、CI待ちがめちゃくちゃ長いww

テキスト入力などを行う Node#set のパラメータ仕様が複雑怪奇

テキスト入力も、チェックボックスも、ラジオボタンも、select/optionも、全部 Node#set というメソッドが担っている。その結果、当然ながら各種ドライバは、どれも Node#set が複雑になってしまっている。

残念ながら、これを真似するほかない。

キーボード入力をする send_keys のパラメータ仕様も複雑怪奇

  • 配列かもしれないし、文字列かもしれない
  • 配列の場合、各要素はシンボルかもしれないしテキストかもしれないし、 [:ctrl, 'C'] のような複合キーを表す配列かもしれない
  • 途中で :shift があったら、それ以降のテキストは大文字にする

などなど、なかなかに配慮が必要。

しかもsend_keysはDriverとNodeの2箇所に定義が必要だ。どのDOM要素にフォーカスがあたっているかを特に気にせずキーボード操作する用のと、DOM要素に対してキーボード操作する用の。

Capybaraのソースを読んでの発見

Tweetそのままだが、 自動操作をCDPをベースにしたもの(= PuppeteerやPlaywright)でもCapybaraを通すと精度が出ない! ということだ。

ようするに、 selenium-webdriverをやめて CupriteApparition を使うだけでは、システムテストの精度は改善しない

Capybaraドライバを実装時に、実際にGitHubリポジトリ検索をCIで動作確認するようにしていたのだけど、

  Capybara.app_host = 'https://github.com'
  visit '/'
  fill_in('q', with: 'Capybara')
  sleep 1 # これがないと、時々失敗する
  find('a[data-item-type="global_search"]').click

Capybara DSLだけでは、このようにお粗末なsleepが必要だった。これをPlaywrightネイティブの page.click('a[data-item-type="global_search"]') に置き換えるだけで、それ以降は(sleep 1をなくしても)一度も失敗しなくなった。

ともあれ、とりあえずリリースしてみた

まだまだプレビュー版だけど、gem pushしてみた。

rubygems.org

こんなかんじで従来のselenium-webdriverと似たような感じでセットアップして

f:id:YusukeIwaki:20210518014031p:plain

Firefox(のマイクロソフト独自改造版のjuggler)がポンポン起動して自動試験が走るようになる。

f:id:YusukeIwaki:20210518014406g:plain

今後...

ぶっちゃけ、(Railsじゃなければ)PlaywrightでE2Eテストを書くなら、playwright-pytestを使えばいいし、自分自身もブラウザの自動操作はだいたいplaywright-pythonで書いているw

とはいえ、Railsのsystem specの運用に不満を抱える状況自体はそう簡単に解消しないだろうし、もしPlaywrightでRailsシステムテストが改善できるなら、いろんな人を幸せにできて良さそうだなとも感じている。

現状はplaywright-ruby-clientのドキュメントが未整備なので、「Railsユーザで、Playwrightを楽しんでみたい方はどうぞw」と言われてもcapybara-playwright-driverは多分まともに使えるのが自分しかいない状態だろう。

そんなわけで、今後はplaywright-ruby-clientのドキュメントを少しずつ整備して、長期的にはRailsシステムテストでPlaywrightを使う人を増やしていきたい。

糸島のパン屋 "のたり" のネット通販を利用してみた

久々に、エンジニアリングじゃないネタ。

のたりのパン

過去にパンを自分で焼く重要性を説いていた。

yusukeiwaki.hatenablog.com

しかしそんな自分だが、実は糸島にある "のたり" というパン屋に出会ってからはすっかり自分で焼くのをやめてしまった。

どんなパンを出しているお店かは、ネットで調べたら出てくるのでそれに任せるが、のたりのパンは、ほとんどが200円〜500円程度だ。

tabelog.com

過去記事で紹介したオレオレパンは1個つくるのに350円くらいかかるわけだが、はるかに丁寧に美味しく作られたパンがこの価格。さすがにパンを自分で焼くモチベーションを全部持っていかれてしまったww

1月からネット販売を開始したみたい

ある日、お店に行くと、ホームページを開設してネット販売を始めたらしいことを聞いた。もともと、blogger という近年まれに見るブログサービスでこぢんまりと情報が発信されていたのだけど、なんとも立派なホームページが作られている。

notari-notari.studio.site

notari-notari.stores.jp

で、気になるネット販売だが、とくにstores.jpのシステム利用料分を上乗せなどなく、店舗のパンの値段がほぼそのままで売られている。送料が550円だが、糸島の桜井までの往復の交通費よりは安い。十分にペイできるだろう。

おまかせセットを買ってみた

自分の好きなパンだけ注文する、でもよかったのだけど、なんとなくおまかせセットを頼んでみた。

  • くるみといちじく
  • カカオ
  • ライ麦カンパーニュ

あたりが、おまかせセットを頼んで入っていなかったらどうしようとか考えてしまったのだけど、奇跡的に(?)全部入っていたww

たぶん偶然なんだろうけど。

f:id:YusukeIwaki:20210424145522p:plain

普段、自分では買わないパンが実はとてもおいしいという新たな発見も。おまかせセットは本当に大満足だ。

感想

いよいよPuppeteerを使う理由がなくなってきた

空き時間を活用しつつ、 puppeteer-rubyplaywright-ruby-client という2つのOSSを育てている。puppeteer-rubyGoogle Chromeチームによる元祖Puppeteer をまるっとRubyで再実装したライブラリで、playwright-ruby-clientは PuppeteerからフォークしてMicrosoft(のなかの元Chromeチームメンバー)が育てているPlaywright のクライアントAPIRubyで実装したライブラリだ。

yusukeiwaki.hatenablog.com

この記事を書いたときには、「Playwrightは自身が起動したブラウザしか自動操作できない」という大きな欠点を挙げていた。

しかし、その数週間後、Playwright 1.9.0で browserType.connectOverCDP というAPIが追加されて、こいつがまさに puppeteer.connect 相当のことをやってくれるようになった。

github.com

つまり、「Seleniumの補強として使う」ということが、PuppeteerのみならずPlaywrightでもできてしまうようになった。

f:id:YusukeIwaki:20210419135049p:plain

現在はpuppeteer-rubyとplaywright-ruby-clientの2つのOSSを現在育てているけど、今後は本格的にplaywright-ruby-clientのほうを注力していくことになるかもしれない。