Edited at
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に積極的にロックインされていくぞ!!という気持ち


参考