Headless ChromeをDocker上で動かして、E2Eのテスト

この記事は[Gunosy Advent Calendar 2017](https://qiita.com/advent-calendar/2017/gunosy)の14日目の記事です。
昨日は@harahenikuさんの[Goを遅くしないための地味な話](https://medium.com/@haraheniku/go%E3%82%92%E9%81%85%E3%81%8F%E3%81%97%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AE%E5%9C%B0%E5%91%B3%E3%81%AA%E8%A9%B1-d20c0fe8b163)でした。

広告技術部の@hoshitocatです。 主な業務はRailsで広告の入稿やレポートのための管理画面の開発をしております。たまにGoで配信側のAPIを触ることがあります。

今回は管理画面でChromeのHeadlessモードを使ったE2Eのテストを導入した話をします。

# PhantomJSからChromeへ
私たちはこれまでE2Eのテストを[PhantomJS](http://phantomjs.org/)を使っていました。
しかし、Javascriptの動作も含めたテストを書いている際に、[includes()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)がPhantomJSだと動作しないことがわかりました。

プロダクトコードをテストコードに合わせて書き直すことも考えましたが、それでは新しく使えるようになった便利な機能をいつまでも使えなくなってしまいます。
そもそもPhantomJSは以下のようにメインメンテナーが開発を今後しないことを公表しています。(https://groups.google.com/forum/#!forum/phantomjs)

> I think people will switch to it, eventually. Chrome is faster and more stable than PhantomJS. And it doesn't eat memory like crazy.

また、上記のように今後は[ChromeのHeadlessモード](https://www.chromestatus.com/feature/5678767817097216)に期待している旨も書いています。

Rails側でも5.1から導入されたSystemTestCaseではデフォルトでサポートしているブラウザがChromeのようです。

# インストール

**環境は[こちらのImage](https://hub.docker.com/r/library/ruby/tags/2.3.5/)を使用しています。**
（Mac上だとbrewとかで簡単にインストールできたと思います。）

zipファイルで配布されている `chromedriver` をダウンロードする

```bash
// 最新バージョン取得
$ CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`
// ZIPファイルダウンロード
$ wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/
```

ファイルを解凍して、権限を付与し `/usr/bin` に移動します

```bash
$ unzip ~/chromedriver_linux64.zip -d ~/
$ rm ~/chromedriver_linux64.zip
$ chown root:root ~/chromedriver
$ chmod 755 ~/chromedriver
$ mv ~/chromedriver /usr/bin/chromedriver
```

[Google Chromeは3rdPartyRepository](https://www.ubuntuupdates.org/ppa/google_chrome)のようなので、追加して上げる必要があります。

また、認証に失敗するので[鍵の登録](https://www.google.com/linuxrepositories/)も必要みたいです。

```sh
sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -'
sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
```

アップデートしてインストールします。

```sh
$ apt-get update -y && apt-get install -y google-chrome-stable
```

以下が実際に作成した `Dockerfile` になります。

```Dockerfile:Dockerfile
FROM ruby:2.3.5

ENV LANG C.UTF-8
ENV APP_HOME /usr/src/app

RUN mkdir -p $APP_HOME
RUN gem update --system
RUN gem install bundler && gem update bundler
RUN apt-get update && apt-get install -y unzip && \
    CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
    wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
    unzip ~/chromedriver_linux64.zip -d ~/ && \
    rm ~/chromedriver_linux64.zip && \
    chown root:root ~/chromedriver && \
    chmod 755 ~/chromedriver && \
    mv ~/chromedriver /usr/bin/chromedriver && \
    sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' && \
    sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \
    apt-get update && apt-get install -y google-chrome-stable

WORKDIR $APP_HOME

COPY Gemfile \
     Gemfile.lock \
     $APP_HOME/
```

# 速度

全体を通してちゃんとしたベンチマークはしていませんが、テスト全体で10分~15分程度のものが12分~17分くらいになりました。2分~3分程度遅くなった感じです。

# Sandboxオプションをつけないと動作しない
ハマった点としては、Javascriptを動作させるようなテストの場合 `sandbox` オプションを付与する必要がありました。
https://sites.google.com/a/chromium.org/chromedriver/help/chrome-doesn-t-start

> Otherwise, if the problem only occurs in your special testing environment:
Using Chrome's alternate installer. This will install Chrome for all users. This often fixes problems if you are running Selenium as a background service.
Passing '--no-sandbox' flag when creating your WebDriver session. Special test environments sometimes cause Chrome to crash when the sandbox is enabled.

特別なテスト環境とはなんのことを言っているのか特定はできませんでしたが、セキュリティ的な問題なのでしょうか？ :thinking: 

以下のようにオプション追加してあげることで解決し、無事に手元で実行することができました。

```ruby:rails_helper.rb
Capybara.register_driver :selenium do |app|
  Capybara::Selenium::Driver.new(app, {
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      chrome_options: {
        # NOTE: ここでno-sandboxオプション追加する
        args: %w[no-sandbox headless disable-gpu window-size=1680,1050],
      }
    )
  })
end
Capybara.javascript_driver = :selenium
```

ちなみにこの問題は開発環境上で発生しただけで、CI上では再現しなくハマってしまいました。

# まとめ

今回はHeadlessモードのChromeをDocker上で動かしてみました。最新のブラウザでサポートされている機能が使えるようになり、プロダクトコードを変更しなくても素直にテストを書くことができました。（polyfillとか車輪の再発明的なことはしたくないですよね。）

# 参考URL
- [Running browser tests - 2.0 Support - CircleCI Community Discussion](https://discuss.circleci.com/t/running-browser-tests/10998)
- [ziadoz/install.sh](https://gist.github.com/ziadoz/3e8ab7e944d02fe872c3454d17af31a5)