こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近は子育てに忙しくしています 👶
先日、メドピアで利用しているcapybaraのjavascript driverをpoltergeistからheadless chrome(selenium-webdriver)に移行しました。driverを変更するにあたって既存のテストコードをいくつか修正する必要があったので、そこで得た学びを共有したいと思います。
なぜ移行したのか
ここ数年、Railsでエンドツーエンドのテストを書くときにはpoltergeistを使う、というのがデファクトスタンダードだったはずです。それ以前はみんなcapybara-webkitを使っていましたが、poltergeistはバックエンドにPhantomJSを使っており、Qtに依存しているcapybara-webkitと比べてビルドが簡単だったことから徐々にシェアを増やしていったように思います*1。
そんな中、だいたい1年ほど前にChromeのバージョン59からヘッドレスモードが実行できるようになりました。これをうけて、PhantomJSの開発が停止されました。PhantomJSをメンテするのはだいぶ大変だったみたいです。
これは当然、依存しているpoltergeistにも影響します。PhantomJSの依存をやめる構想もあったようですが、リポジトリを眺めている限りではあまり進捗しておらず、さらにpoltergeistの開発自体あまりアクティブではないように見えます。
ここまでの経緯を考えると、headless chromeに切り替えるのは無難な選択と思われるのですが、移行時期には考慮が必要でした。headlessモードが使えるchromeがリリースされた当初は、chromedriver(chromeを別プロセスから動かすのに必要なもの)に大きめのバグがあり、すぐに移行するには不安な状況でした。しかしそれから1年ほど経過した今なら問題なく移行できるのではないか?となったのでした。
そんなわけで、フィーチャスペックのときに使うドライバをpoltergeistからheadless chromeに変更しました。
移行に必要なこと
リリースから1年経ってこなれたと言っても、poltergeistからheadless chromeへの変更は単純に入れ替えただけでは完了しません。テストコードの変更も必要です。
僕たちはcapybaraのインターフェースを経由してheadless chromeやpoltergeistを利用しているので、移行してもテストの書き方は基本的には変わりません。ただ、ドライバによって対応しているメソッドとしていないメソッドがあるため、poltergeistのときは動いていたコードがheadless chromeでは動かないケースがあるのでした。そういうところは動くように書き換えてあげる必要があります。
大抵の箇所は機械的に置換していくようなやり方でOKなのですが、テストの内容によってはもっと頑張らなければならないところがあるのでそれを紹介します。
ファイルダウンロードのテストを変更する
例えばリンクをクリックするとcsvがダウンロードできるようなページがあったとします。poltergeistを利用している場合、次のようにレスポンスヘッダを見て、Content-Typeが text/csv であることをもってテストを成功をみなすという方法があります。
context 'CSVダウンロード用のページに遷移したとき' do before { visit csv_download_path } it 'かつ"CSVダウンロード"をクリックしたらCSVファイルがダウンロードできること' do click_on 'CSVダウンロード' expect(page.response_headers['Content-Disposition']).to eq("attachment; filename=\"download.csv\"") expect(page.response_headers['Content-Type']).to eq("text/csv") end end
上記のコードは、headless chromeに切り替えるとうまく動きません。どうやらheadless chrome(正確にはselenium webdriver)はレスポンスヘッダを見るAPIを提供していないようです。バグとかではなくそういう設計方針の模様。
レスポンスヘッダを見れないとしたら、実際にダウンロードしたファイルの内容を確認するしかありません。こちらのエントリを参考にしつつ、実際にダウンロードしたファイルの中身をチェックする方法に変更しました。
まず、次のようなヘルパーメソッド群をモジュールとして定義し、フィーチャスペックで使えるようにします。
module DownloadHelper TIMEOUT = 10 PATH = Rails.root.join('tmp/downloads') module_function def downloads Dir[PATH.join('*')] end def download downloads.first end def download_content wait_for_download File.read(download) end def wait_for_download Timeout.timeout(TIMEOUT) do sleep 0.1 until downloaded? end end def downloaded? !downloading? && downloads.any? end def downloading? downloads.grep(/\.crdownload$/).any? end def clear_downloads FileUtils.rm_f(downloads) end end RSpec.configure do |config| config.include DownloadHelper, type: :feature config.before(:suite) { Dir.mkdir(DownloadHelper::PATH) unless Dir.exist?(DownloadHelper::PATH) } config.after(:example, type: :feature) { clear_downloads } end
これにより、先程のテストを次のように変更することができます。
context 'CSVダウンロード用のページに遷移したとき' do before { visit csv_download_path } it 'かつ"CSVダウンロード"をクリックしたらCSVファイルがダウンロードできること' do click_on 'CSVダウンロード' expect(download_content).to eq "row1,row2\n" end end
さてこれで解決…と思いきや、もうひとつ対処が必要だったりします。どうやら、 headless chromeはデフォルトでファイルダウンロードをしないようです。そこでstackoverflowの回答を参考に、ファイルダウンロードを許可するようにしました。具体的には次のようにconfig/rails_helper.rb
に記述しています(bridge変数以下がファイルダウンロードを許可しているコード)。
Capybara.register_driver :headless_chrome do |app| driver = Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( login_prefs: { browser: 'ALL' }, chrome_options: { args: %w(headless disable-gpu window-size=1900,1200 lang=ja no-sandbox disable-dev-shm-usage), } ) ) bridge = driver.browser.send(:bridge) path = "session/#{bridge.session_id}/chromium/send_command" bridge.http.call( :post, path, cmd: 'Page.setDownloadBehavior', params: { behavior: 'allow', downloadPath: DownloadHelper::PATH.to_s, } ) driver end Capybara.javascript_driver = :headless_chrome
これでファイルダウンロードをheadless chromeでテストできるようになりました。
追記
最近(2019年9月)のchrome77からはこの方法だとテストが失敗するようになっています。詳細について新しい記事にしたので参考にしてください。
最近のheadless chromeを利用したファイルダウンロードのテスト方法について - メドピア開発者ブログ
ブラウザ上で見えない要素に対応する
headless chromeは、poltergeistと比べて「ブラウザ上で見える要素であるか否か」にシビアなようです。例えば、position: fixed; left: 1000px
としているDOM要素があるとします。このとき、ブラウザのウィンドウサイズが(800, 600)のように小さく要素が画面外になる場合は、その要素は見えないという扱いになります。
この場合は単にウィンドウサイズを大きくしてあげればよいのですが、それでは対応できない場合は次のようにvisible: false
をつけて、見えない要素の中で指定のDOMが存在するかを確認する必要があります。
expect(page).to have_css('.super-right-dom', visible: false)
このように「画面外にあるので見えない」というのはわかりやすいのですが、なにをもって「見える」「見えない」と判断しているのか難しいなと感じるケースがあります。例えばあるページではbxSliderを利用して画像をカルーセル表示しているのですが、スクリーンショットで見えていることが確認できるDOM要素もheadless chrome的には見えない扱いをされていました。
やむを得ずこれもvisible: false
として対応しました。しかしこのあたりはまだ深く調査できてないので、詳しい方いたら教えていただけると嬉しいです。
細かい変更点
次のような細かい点にも対応しました。このへんは単に置換するだけなので軽く箇条書きで済ませます。
- triggerメソッドがない
- trigger('click')を使わず、clickメソッドを使うようにした
- ウィンドウのリサイズのお作法が違う
- before:
Capybara.page.driver.browser.resize(320, 580)
- after:
Capybara.current_session.current_window.resize_to(320, 580)
- before:
- ブラウザのダイアログが表示される部分は、polgergeistでは自動でacceptされるが、headless chrome(というかselenium webdriver)ではどのように対応するかを
accept_alert {}
みたいに書く必要がある
まとめ
polgergeistからheadless chromeへの移行において、気をつける点と変更が必要な点について紹介しました。headless chromeに移行することで、今後メンテナンスが続いていくであろうという安心感が得られます。しかし、セットアップには今回紹介したようにコツをつかむ必要があります。
この記事がこれからheadless chromeへ移行する人や、移行に苦労している人にとって参考になれば幸いです。
是非読者になってください(︎ ՞ਊ ՞)︎
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら
https://medpeer.co.jp/recruit/workplace/development.html
Rails未経験/経験年数が2年以内のポテンシャルエンジニア絶賛募集中です!*2
Rails開発してみたい人、新しい技術に意欲的な人、リードエンジニアに教育されてみたい(?)人、医療に関わるサービス開発を行ってみたい人、その他メドピアに興味を持った方などは、是非、コンタクトを取って頂ければと思います!