Selenium
Capybara
GitLab
docker
GitLab-CI
GitLabDay 2

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

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

前提・環境

  • オンプレではなく、SaaS版のGitLabを使用
    • GitLab Runnerは特に自前では立てず、Shared Runnersを利用してます。
  • Dockerベースのローカル開発環境
  • テスト対象はAP-DB構成のシンプルなRails構成としてます

検討

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

Seleniumの利用方針

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

Seleniumを利用するSaaS

たくさんありましたが、結論から言うと実際の利用には至ってません。
ライセンス体系や細々としたところで差異があり、簡易な比較表を以下の通り作成しています。(調査時点での情報ですので、最新情報についてはそれぞれのサイトをご覧ください)
ブラウザテストSaaS比較 (3).jpg

SWATHub Sauce Labs Testing Bot BrowserStack Appveyor CrossBrowserTesting
日本語対応 × × × × ×
日本語情報 ◯(公式が日本語、サポートも日本人) ×寄りの△ × ×(古い情報しかない)
公式ドキュメント
価格(全て月額) ¥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) -
並行テスト -
その他 画像比較ができる
実行ノードとしてBrowserStackやSauce Labsを利用できる
ブラウザ,OSのバージョンが圧倒的ラインアップ Sauce Labsを上回るレベル
設定の自動生成も行える
サンプルが充実している

実装

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

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を参考にしました。

テスト用コード

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

Rspec

E2Eの手段としてFeature Specではなく、こちらで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

肝はoptionshost!でしょうか。
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でDockerイメージを指定すると、それらのコンテナが自動的に同一ネットワーク上で起動されるので、要はdockerコマンドの--netオプション相当のことが自動で行われるので、こんなほぼ脳筋な方法よりGitLabの便利なやり方に乗っ取りたかったのですが、

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

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

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

実際Railsアプリ用のイメージを毎回フルビルドするのはあまり効率的では無いので、こちらを参考に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に積極的にロックインされていくぞ!!という気持ち

参考