RSpec の feature spec でヘッドレス Chrome を使う

Speee エンジニア組織推進室の服部 (yhatt) です。

みなさん E2E テストされていますでしょうか。弊社の Ruby on Rails プロダクトにおいては、RSpecCapybaraPoltergeist を組み合わせ、 feature spec で E2E テストを行う構成が一般的でした。

そんな中、Chrome 59 に ヘッドレスモード (--headless) が搭載 されたことで、テストや CI 環境において、最新の Chrome 環境による E2E テストを実施できるようになりました。それに合わせて、PhantomJS のコアメンテナーがメンテナーを降りる ことを発表し、PhantomJS のアップデートや、継続的サポートは期待できない状況となっています。

そこで今回、社内プロダクトのRSpec における feature spec の実行環境を Poltergeist から ヘッドレス Chrome に移行しましたので、その手順をご紹介いたします。

PhantomJS (Poltergeist) の利用につきまとう問題

PhantomJS は古い Webkit をベースにしているため、最新の JS/CSS の機能に追随できていません。より厳密に言えば、PhantomJS は Qt のコンポーネントである QtWebKit を使用していますが、Qt ではすでに QtWebEngine に移行しています。

実際にあった例として、『ベンダープレフィックスの無い Flexbox を使ったら feature spec が落ちた』ということがありました。今回適用した社内プロダクトの開発では、対応ブラウザを限定できていた ため、実戦投入可能という判断で、ベンダープレフィックス無し Flexbox の採用に踏み切りました。

しかし、PhantomJS では『ただのブロック要素としてレンダリングされてしまった結果、すぐ下にあったフォームの上に覆い被さった』という現象が発生し、該当フォームのクリックを含む feature テストが落ちてしまいました。

f:id:yuki-hattori:20170615104248p:plain:w500

autoprefixer を入れて対処するという選択肢もありましたが、CI のためだけに対応バージョンを広げるのは本質的ではない解決策のため、ヘッドレス Chrome の採用に舵を切りました 。

安定版の Chrome をテスト環境に採用することで、『よりエンドユーザーの環境に近い状態でテストを回せる』というメリットもあります。

ヘッドレス Chrome への移行

もしすでに Capybara を使って feature spec のテスト環境を導入していれば、ドライバを変更するだけで実行環境を変更できるので、移行自体はそんなに難しくありません。

以下、『Poltergeist による feature spec テスト環境が導入されている』という前提に基づいて説明していきます。

selenium-webdriver gem を導入

まず、Capybara で feature spec を実行するドライバを Poltergeist から Selenium に変更します。

 group :development, :test do
   gem 'rspec-rails'
   gem 'capybara'
-  gem 'poltergeist'
+  gem 'selenium-webdriver'
   ...
 end

Gemfile を編集したのち、bundle install を実行してください。

capybara + selenium-webdriver の構成は、Rails 5.1 から標準構成になりました ので、新規に Rails アプリを作った場合、この 2 つの gem は特に意識せずとも入っているはずです。

Capybara のドライバ設定で ヘッドレス Chrome を使う

続いて、Capybara のドライバを Selenium 経由で ヘッドレス Chrome を使用するように設定します。

Poltergeist の設定を外した上で、spec/support/capybara.rb に以下の内容を記述します。

require 'capybara/rspec'
require 'selenium-webdriver'

Capybara.register_driver :selenium do |app|
  Capybara::Selenium::Driver.new(app,
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      chrome_options: {
        args: %w(headless disable-gpu window-size=1680,1050),
      },
    )
  )
end

Capybara.javascript_driver = :selenium

※ 事前に spec/rails_helper.rb の23行目 にある Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } がコメントインされていないと、正しく動作しません。

chrome_optionsargs 1 に、headless というオプションが含まれているのを確認してください。ヘッドレス Chrome ことはじめ  |  Web  |  Google Developers によると、現在の最新安定版である Chrome 59 では disable-gpu オプションも必要です。

ウィンドウサイズは必須ではありませんが、レスポンシブなコンテンツなどにおいては挙動が変わってしまう可能性もありますので、テスト環境を揃えるために指定しておくことをお勧めします。

テスト実行環境に ChromeDriver を入れる

テストを実行する前に、忘れずに ChromeDriver を入れておきましょう。最新バージョンの 2.30 が Chrome 59 に対応しています。

macOS ユーザーであれば、Homebrew で導入しても構いません。

$ brew install chromedriver

テスト実行

以上で、ヘッドレス Chrome で feature spec を実行する準備ができました。試しに js: true のテストだけ、狙い撃ちで実行してみましょう。

bundle exec rspec --tag js

実行中は Chrome のインスタンスが立ち上がり、macOS 環境の場合、Dock にアイコンが表示されることが確認できます。

f:id:yuki-hattori:20170615095247g:plain

無事テストが通れば万々歳ですが、現実的には、修正が必要な箇所(=Poltergeist では動くが Selenium で動かない箇所)も少なくないと思いますので、そのようなテストは少しづつ潰していきましょう。例えば、以下のようなテストに要注意です。

  • ドライバ固有のメソッドをテスト中に使用している
    • テスト中に driver を直接使っていたら注意!
  • ウィンドウ関連の操作
    • 新規ウィンドウを開く、リサイズするなど
  • モーダル関連
    • window.alert() window.confirm() など
    • 現時点では Capybara を適切に設定にすることで回避可能 1
      • [2017/06/19追記] Capybara 2.14.3 のリリースにより、ほぼ問題は出なくなりました

ヘッドレスモード中にスクリーンショットを撮る

Chrome 59 のヘッドレスモードで、Capybara の #save_screenshot を使用してスクリーンショットを撮影しようとすると、1x1 の何もない画像が撮れてしまいます。

f:id:yuki-hattori:20170615095522p:plain:w500

これは Chrome 側の問題のようで、以下の Issue を見る限り、既に解決している模様です。

この変更がリリースされるまでは、『スクリーンショットを撮るときは ヘッドレスモードを OFF にする』というのが暫定対策となりますが、待てない方は Google Chrome Canary をテストで使用すると良いでしょう。2

Google Chrome Canary をテストに使用する
Capybara.register_driver :selenium do |app|
  Capybara::Selenium::Driver.new(app,
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      chrome_options: {
        args: %w(headless disable-gpu window-size=1680,1050),
        binary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
      },
    )
  )
end

binary で、実行する Chrome のパスを指定することができます。上記は macOS 環境での例です。

一応、Canary は 常に更新され続ける(毎日更新される)開発版を使用することになる ので、安定版とは挙動が異なる可能性もあることに注意してください。

ベンチマーク

簡易ですが、rspec --tag js -p コマンドにてベンチマークをとってみました。対象は 12 examples の小規模なプロジェクトで、Chrome は安定版 (59) を使用しています。

環境 rspec コマンド実行時間 examples 実行時間
Poltergeist (PhantomJS) 17.81 sec 11.65 sec
Selenium (Chrome 59) 22.75 sec 15.97 sec

rspec --tag js -p を 5 回試行した平均値
※ examples 実行時間 = Total time - files took seconds to load

Poltergeist + PhnatomJS に比べて、ヘッドレス Chrome は コマンド全体で 約 1.37 倍の時間増 となりました。

ヘッドレス特化ブラウザ → フルブラウザ による時間増は仕方ない部分もありますが、体感では、#visit#click などによる ページ遷移 で速度差を感じることが多かったです。例えば、4 回のページ遷移を含む example では、2.71sec (PhantomJS) → 4.32sec (Chrome) と、約 1.59 倍 にまで遅くなりました。

ユニットテストが中心で、E2Eテストがある程度絞られている 小規模〜中規模のプロジェクトだと 気にならないかもしれませんが、大規模なプロジェクトで E2E テストが多く書かれている場合、この時間増がけっこう響きそうです。

まとめ

RSpec の feature spec をヘッドレス Chrome へ移行する手順について解説しました。

これまでも Xvfb を使うことで、実際のブラウザを使ってヘッドレスに自動テストができましたが、今回のヘッドレスモードの実装で、CI でも開発環境でも、より簡単にヘッドレスなテストが実現できるようになり、ハードルが随分下がった印象を受けました。

また、Firefox にもヘッドレスモードが搭載される予定 のようで、今後の E2E テストは、エンドユーザー向けのブラウザを直接動作させる形に収束していくのでは? と思っています。

それでは良き E2E テストライフを。

参考記事


  1. [2017/06/19修正] 初稿では、 args がハッシュロケット記法になっていました。Capybara 2.14.2 時点では、Capybara の ヘッドレス Chrome 判定が特定の構造の Hash しか受け付けない ようになっており、Symbol キーにしてしまうと accept_confirmdismiss_confirm の挙動に影響が及ぶ(該当 Issue comment…)ためです。この問題は 2.14.3 で修正されています。該当 Commit…

  2. Chrome は ベータ版 や開発版も提供していますが、既存の Chrome 安定版を上書きする上、戻すのも面倒なので、開発環境での導入はあまりお勧めできません。Google Chrome Canary であれば、安定版と共存することが可能です。