GitLab CI上でRailsアプリをRSpec(System Spec)でdocker-seleniumを利用してE2Eテストした話

GitLab上でE2Eテストを回したいとなった時に色々と調査・検討したのでまとめたいと思います。

# 前提・環境
* オンプレではなく、SaaS版のGitLabを使用
    * [GitLab Runner](https://docs.gitlab.com/ee/ci/runners/)は特に自前では立てず、Shared Runnersを利用してます。
* Dockerベースのローカル開発環境
* テスト対象はAP-DB構成のシンプルなRails構成としてます

# 検討
E2EといえばSeleniumだろう、というところはチーム全員が経験者ということもあり思考停止で決め打ちしています。(Cypressとかは出てきません)
Seleniumのランタイムをどうするか、というところをまず検討しました。

## Seleniumの利用方針
* Dockerベースのローカル開発環境を前提としており、かつalpine-linuxベースのimageだったので、まずはその前提でローカルでSeleniumを動かそうと[こちら](https://qiita.com/dd511805/items/d66d59df2a08a0982850)の記事等を参考に色々試したのですがついぞまともに動かず、かつブラウザバリエーションも担保しづらいのでリモートのものを利用する（Selenium Gridを利用）こととしました。
* Selenium Gridを利用するとなった場合に、公式の[`SeleniumHQ/docker-selenium`](https://github.com/SeleniumHQ/docker-selenium)というものが結構よさげだったのでそれを採用しました。

## Seleniumを利用するSaaS
たくさんありましたが、結論から言うと実際の利用には至ってません。
ライセンス体系や細々としたところで差異があり、簡易な比較表を以下の通り作成しています。（調査時点での情報ですので、最新情報についてはそれぞれのサイトをご覧ください）
![ブラウザテストSaaS比較 (3).jpg](https://qiita-image-store.s3.amazonaws.com/0/115377/b84dde85-2f1e-198a-db56-40cf994b4ce3.jpeg)

||[SWATHub](http://www.smartekworks.com/swathub.html)|[Sauce Labs](https://saucelabs.com/)|[Testing Bot](https://testingbot.com/)|[BrowserStack](https://www.browserstack.com)|[Appveyor](https://www.appveyor.com/)|[CrossBrowserTesting](https://crossbrowsertesting.com)|
|:--|:--:|:--:|:--:|:--:|:--:|:--:|
|日本語対応|◯|×|×|×|×|×|
|日本語情報|◯(公式が日本語、サポートも日本人)|×寄りの△|×|△|◯|×(古い情報しかない)|
|公式ドキュメント|◯|△|◎|◎|◯|◯|
|価格(全て月額)|¥30,000/100画面|$149/2session(実行時間無制限だと$298/2session,どのプランも年単位契約)|$90/session(monthlyだと$120)|$99/session(スマフォビューも含むと$149/session,どのプランも年単位契約)|-|$100/session(monthlyだと$120)|
|対応デバイス|Win,Mac,iOS,Android|Win,Mac,OSX,Linux,iOS,Android(スマフォは実機も有)|Win,Mac,OSX,Linux,iOS,Android|Win,Mac,OSX,Linux,iOS,Android|-|Win,Mac,OSX,Linux,iOS,Android(スマフォは実機も有)|
|対応ブラウザ|IE,Edge,Chrome,FF,Safari|IE,Edge,Chrome,FF,Safari|IE,Edge,Chrome,FF,Safari,Opera|IE,Edge,Chrome,FF,Safari,Opera|-|IE,Edge,Chrome,FF,Safari|
|対応アプリ形式|Web|Web,Mobile Native|Web,Mobile Native|Web,Mobile Native|-|Web|
|対応Selenium ver|2系,3系|2系,3系|2系,3系|2系,3系|-||
|スクリーンショット|◯|◯|◯|◯|△（SeleniumのAPIを自身で叩くしか無い）||
|動画保存|×|◯|×|◯|-|◯|
|Live Testing(SaaS上の仮想環境上のブラウザでの実行状況を確認できる機能)|×|◯|×|◯|-|◯|
|Local Testing(ローカルで実行できる機能)|◯|◯(Sauce Labs Connect)|◯(TestingBotTunnel)|◯|-|◯|
|並行テスト|◯|◯|◯|◯|-|◯|
|その他|画像比較ができる<br>実行ノードとしてBrowserStackやSauce Labsを利用できる||ブラウザ,OSのバージョンが圧倒的ラインアップ|Sauce Labsを上回るレベル<br>設定の自動生成も行える<br>サンプルが充実している|||


# 実装

## dockerを利用したローカルでの開発環境
### `Dockerfile`

```Dockerfile
FROM ruby:2.5-alpine

WORKDIR /work/

RUN apk update && \
    apk upgrade && \
    apk add --no-cache build-base libxml2-dev libxslt-dev postgresql-dev \
            nodejs tzdata imagemagick git openssh-client curl

ADD Gemfile Gemfile.lock ./

RUN bundle install --without production

RUN gem install ffi --no-doc --no-ri

RUN rm -f tmp/pids/server.pid

CMD bundle exec rails s -p 3000 -b '0.0.0.0'
```

### `docker-compose`

```docker-compose.yml
version: '3'

services:
  postgres:
    image: postgres:9.6
    environment:
      POSTGRES_USER: "root"
    ports:
      - "5432:5432"

  rails:
    build: .
    volumes:
      - .:/work
    environment:
      - RAILS_ENV=development
      - APP_HOST=rails
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
    ports:
      - "3000:3000"

  selenium-hub:
    image: selenium/hub:3.14
    container_name: selenium-hub
    ports:
      - "4444:4444"

  chrome:
    image: selenium/node-chrome:3.14
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
```

selenium-hubの定義の仕方とかは、[公式のREADME](https://github.com/SeleniumHQ/docker-selenium)を参考にしました。

## テスト用コード
Railsアプリなので基本的にCapybara経由でSeleniumを触ることを前提とします。
また、実際のテストコードについては割愛し、`docker-selenium`と`Capybara`の組み合わせ方のところが明確なドキュメンテーションがなかなかなかったのでそちらを記載します。

### Rspec
E2Eの手段としてFeature Specではなく、[こちら](http://rspec.info/blog/2017/10/rspec-3-7-has-been-released/)でRails5.1以降で推奨されている、System Specの利用を前提としてます。

```rails_helper.rb
require 'capybara/rspec'
require 'selenium-webdriver'

options = ENV.has_key?('HUB_HOST') ? { url: "http://#{ENV['HUB_HOST']}:#{ENV['HUB_PORT']}/wd/hub" } : {}

Capybara.run_server = false

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium,
              using: :chrome,
              screen_size: [1400, 1400],
              options: options

    host! "http://#{ENV.fetch('APP_HOST', 'localhost')}:3000"
  end

  # その他雑多な定義は省略

end
```

肝は`options`と`host!`でしょうか。
`url`オプションは、~~デフォルトの`localhost:{各ブラウザDriver毎のデフォルトポート}`というURL、つまりローカルのWebDriver経由でテスト実行をしたくないときに指定します。~~リモート(Remote WebDriver)でテストを実行したい時に指定します。
実際に定義されているURLは、selenium-hubコンテナ上で稼働しているのSelenium Grid Hub(=実態としてはSelenium Server)のURLです。
`host!`メソッドの引数はなんでこういう書き方になっているかというと、ローカルでRailsアプリを起動してローカルのWebDriver経由でローカルのブラウザに対してテストしたいメンバがいたためです。

### Minitest

```application_system_test_case.rb
require 'test_helper'
require 'capybara/rails'
require 'capybara/minitest'
require 'selenium-webdriver'

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  options = ENV.has_key?('HUB_HOST') ? { url: "http://#{ENV['HUB_HOST']}:#{ENV['HUB_PORT']}/wd/hub" } : {}

  driven_by :selenium,
            using: :chrome,
            screen_size: [1400, 1400], 
            options: options

  Capybara.run_server = false

  def setup
    host! "http://#{ENV.fetch('APP_HOST', 'localhost')}:3000"
  end

  # その他雑多な(ry

end
```

各SystemTestCaseが上記`ApplicationSystemTestCase`クラスを継承して実装していく感じになります。
細かいところはRSpec版と同じなので説明は省略します。

## GitLab CI
前置きが長くなってしまいましたが、GitLab Advent Calendarの本命です。

### `.gitlab-ci.yml`ミニマム版

```.gitlab-ci.yml
services:
  - docker:dind

variables:
  GIT_SUBMODULE_STRATEGY: normal
  POSTGRES_USER: root
  DOCKER_DRIVER: overlay2

rails_e2e_test:
  stage: test
  image: docker
  retry: 2
  artifacts:
    paths:
      # capybaraを使うとデフォルトで以下のディレクトリに失敗時のスクリーンショットが残るので、失敗結果を確認できるようにキャッシュしておく
      - tmp/screenshots
    when: always
    expire_in: 4 weeks
  variables:
    APP_HOST: rails
    RAILS_ENV: development
    HUB_HOST: selenium-hub
    HUB_PORT: 4444
  script:
    - docker build -t rails-image .
    - docker network create e2e-test
    - docker run -d --net e2e-test --name postgres -e POSTGRES_USER=root postgres:9.6
    - docker run -d --net e2e-test --name rails -e APP_HOST -e RAILS_ENV -e HUB_HOST -e HUB_PORT -v $PWD:/work rails-image
    - docker run -d --net e2e-test --name selenium-hub selenium/hub:3.14
    - docker run -d --net e2e-test --name chrome -e HUB_HOST -v /dev/shm:/dev/shm selenium/node-chrome:3.14
    - docker exec -i rails rails db:reset
    - docker exec -i rails rails spec:system
```

実際にはいろんなミドルウェア使ってるので、もっとdockerコンテナを起動しているのですが、AP-DB構成だと最小構成で上記みたいな感じになるかと思います。
[`services`](https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#available-settings-for-services)でDockerイメージを指定すると、それらのコンテナが自動的に同一ネットワーク上で起動されるので、要は`docker`コマンドの`--net`オプション相当のことが自動で行われるので、こんなほぼ脳筋な方法よりGitLabの便利なやり方に乗っ取りたかったのですが、

* `services`側で定義したイメージに環境変数を食わせられない
    * [こちら](https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#available-settings-for-services)を見たら、`command`を指定して環境変数をexportすることでいけたかもしれません。。。 
* `selenium-node-chrome`イメージの起動時のボリュームマウントの設定を事前にやっておく方法がパッと思いつかなかった。
    * 事前にDockerfileなりに定義しておいて予めビルドし、レジストリにpushしておくとかになるのでしょうか。Docker力が雑魚なのでお詳しい方いらっしゃればご教示ください。。

また、`docker-compose`を利用して実行すると言う方法もあり、確かにそれだとローカルの開発環境と平仄が揃うというメリットがあるので試してみましたが、実行時間が上記のほうがまだ早かったので上記にしました。

### `.gitlab-ci.yml`ちょっと整理した版

実際Railsアプリ用のイメージを毎回フルビルドするのはあまり効率的では無いので、[こちら](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#using-docker-caching)を参考にGitLab内にホストされているDockerレジストリをキャッシュとして利用します。

```.gitlab-ci.yml
services:
  - docker:dind

variables:
  GIT_SUBMODULE_STRATEGY: normal
  POSTGRES_USER: root
  RAILS_CI_IMAGE: registry.gitlab.com/your-group/your-repository/rails-image
  DOCKER_DRIVER: overlay2

# これとは別に定期的にDOckerイメージをフルビルドするジョブを定義してもいいかもしれません
build-ci-from-cache:
  stage: build-ci
  image: docker
  # よくコケるのでretryできるようにしておく
  retry: 2
  script:
    # see https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#using-docker-caching
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
    - docker pull $RAILS_CI_IMAGE:latest || true
    - docker build --cache-from $RAILS_CI_IMAGE:latest -t $RAILS_CI_IMAGE:latest .
    # 最新のビルド済Docker ImageをGitLab上のDocker Registryにpushする
    - docker push $RAILS_CI_IMAGE:latest

rails_e2e_test:
  stage: test
  image: docker
  retry: 2
  artifacts:
    paths:
      - tmp/screenshots
    when: always
    expire_in: 4 weeks
  variables:
    APP_HOST: rails
    RAILS_ENV: development
    HUB_HOST: selenium-hub
    HUB_PORT: 4444
  script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
    - docker pull $RAILS_CI_IMAGE:latest
    - docker network create e2e-test
    - docker run -d --net e2e-test --name postgres -e POSTGRES_USER=root postgres:9.6
    - docker run -d --net e2e-test --name rails -e APP_HOST -e RAILS_ENV -e HUB_HOST -e HUB_PORT -v $PWD:/work $RAILS_CI_IMAGE:latest
    - docker run -d --net e2e-test --name selenium-hub selenium/hub:3.14
    - docker run -d --net e2e-test --name chrome -e HUB_HOST -v /dev/shm:/dev/shm selenium/node-chrome:3.14
    - docker exec -i rails rails db:reset
    - docker exec -i rails rails spec:system
```

# 所感
* `.gitlab-ci.yml`の構成、多分まだまだ改善の余地ある
* もともとオンプレJenkinsユーザで、転職してからSaaSのGitLab CIをまともに触り始めたのですが、CI環境メンテナンス要らないとか素敵で、GitLab使っている以上あまり無理して他のCIサービス使う理由も無いので、当分はGitLabに積極的にロックインされていくぞ！！という気持ち

# 参考
* [GitHub - SeleniumHQ/docker-selenium: Docker images for Selenium Grid Server (Standalone, Hub, and Nodes).](https://github.com/SeleniumHQ/docker-selenium)
* [RSpec 3.7 has been released!](http://rspec.info/blog/2017/10/rspec-3-7-has-been-released/)
* [GitHub - teamcapybara/capybara: Acceptance test framework for web applications](https://github.com/teamcapybara/capybara)
* [WebDriver について私が知っていること (2017 年版) - ひだまりソケットは壊れない](https://vividcode.hatenablog.com/entry/web-driver-introduction-2017)
* [Configuring GitLab Runners | GitLab](https://docs.gitlab.com/ee/ci/runners/)
* [Building Docker images with GitLab CI/CD | GitLab](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)
* [Using Docker images | GitLab](https://docs.gitlab.com/ee/ci/docker/using_docker_images.html)
* [.gitlab-ci.yml によるジョブの設定方法(日本語訳) - Qiita](https://qiita.com/ynott/items/1ff698868ef85e50f5a1)
* [CapybaraからHeadless Chromeを動かす環境をDockerで構築する - Qiita](https://qiita.com/dd511805/items/d66d59df2a08a0982850)