E2E(End-to-End)テストを実行する際、エンジニアのローカル環境によってテスト結果が異なることがあります。これは、それぞれのローカル環境が統一されていないことが原因です(例:OS の違い、ブラウザのバージョンの違いなど)。
この問題を解決するために CI ツールを導入する方法がありますが、そこまで手間やコストをかけたくない場合、ローカル環境で Docker を使ってテストを実行することで、環境による差異をほぼなくすことができます。
この記事では、Ruby on Rails のテストを例に、Docker での E2E テスト実行環境の構築方法を解説します。E2E テストにはアクセプタンス・テストフレームワークの Cucumber を使用しますが、Cucumber は内部で Capybara を使用しているため、Capybara を使用している他のテストツールでも参考になるかと思います。
- 全体像
- 設定
- Gemfile
- Docker compose
- postgresコンテナ
- Cucumberコンテナ
- Chromiumコンテナ
- テスト実行
- まとめ
全体像
詳細な設定に移る前に、全体像を把握しておきましょう。
このセットアップでは以下の3つのコンテナを使用します。
- postgres: Railsのデータベース。開発環境とテスト環境両方のデータを格納します。
- cucumber: E2Eテストの実行。Railsのアプリケーションサーバーを立ち上げると共に、WebDriverプロトコルを使用してchromiumコンテナを遠隔操作してテストを実行します。
- chromium: テスト用ブラウザーとしてChromiumを提供します。
Dockerコンテナ間ネットワークをイメージすると以下の画像のようになります(数字はポート番号です)。

また、ホストマシーンは、RailsのソースコードやDocker設定ファイルを提供します。
設定
Gemfile
Gemfileに以下のgemを記載します。
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15'
gem 'selenium-webdriver'
gem 'cucumber-rails', require: false
gem 'database_cleaner'
end
ホストマシーンでbundle installを実行します。Cucumberコンテナ内部でも別途bundle installを実行するため不要に見えるかもしれませんが、Gemfile.lockを更新するために必要な作業です。
bundle install
Docker compose
次にDocker composeファイルを作成します。
# docker-compose.yml
services:
postgres:
image: postgres:14
container_name: postgres
ports:
- "5432:5432"
environment:
- "POSTGRES_USER=postgres"
- "POSTGRES_PASSWORD=my_password"
volumes:
- postgres_data:/var/lib/postgresql/data
cucumber:
container_name: cucumber
build:
context: .
dockerfile: dockerconfig/cucumber/Dockerfile
environment:
- "DATABASE_URL=postgresql://postgres:my_password@postgres:5432/my_app?encoding=utf8&pool=5&timeout=5000"
- "TZ=Asia/Tokyo"
expose:
- 41111
volumes:
- .:/app/
networks:
default:
aliases:
- my_app.test
profiles:
- e2e-test
# stdin_openとtty属性はコンテナが立ち上がった途端にexitしないために必要。
stdin_open: true # same as -i option in docker run or exec
tty: true # same as -t option in docker run or exec
chromium:
build:
context: .
dockerfile: dockerconfig/chromium/Dockerfile
container_name: chromium
environment:
- "TZ=Asia/Tokyo"
shm_size: 2gb
profiles:
- e2e-test
volumes:
postgres_data:
docker-compose.yml内の各コンテナの解説は以下の通りです。
postgresコンテナ
開発・テスト両方のデータベースを格納します。開発時にRailsをホストマシン側で実行するため、ports属性でhost側にpostgresのデフォルトポート5432を開放しています(テストのみに使用するのであればコンテナ間ネットワークにポートを開放するexpose属性で十分です)。
environment属性で指定したユーザー名とパスワードは、cucumberコンテナ内のRailsのDB設定にも使用します。
postgresサービスのvolumesで指定しているpostgres_dataはトップレベルのvolumesで定義したボリュームと同一で、コンテナをダウンさせた後も永続的にデータを保存するために使用しています(主に開発用)。指定しているパスはコンテナ側のマウントパスです。
Cucumberコンテナ
E2Eテスト実行をするコンテナです。Railsのアプリケーションサーバーを立ち上げると共に、WebDriverプロトコルを使用してchromiumコンテナを遠隔操作してE2Eテストを実行します。
Dockerfileをアプリケーションルートとは別のディレクトリに配置しているため、build以下のdockerfile属性でファイルパスを指定しています。Dockerfileの中身は以下の通りで、Ruby、Nodejs、gemのインストールをするだけのシンプルな内容です。
# dockerconfig/cucumber/Dockerfile
FROM ruby:2.7.8
RUN apt update -qq && \
apt install -y build-essential libvips \
git \
libpq-dev \
curl
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN apt install -y nodejs
RUN gem install bundler:2.1.4
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
environmentで設定しているDATABASE_URL環境変数は、Railsのconfig/database.ymlで使われており、postgresコンテナに接続するためにサービス名とポートを@postgres:5432と指定しています。
exposeでポート番号41111をコンテナ内部ネットワークに開放しています。これは、テスト用のRailsサーバーがこの番号をlistenしているためです。そのため、chromiumブラウザーもこのポートを使ってテストを実行します。
# docker-compose.yml
environment:
- "DATABASE_URL=postgresql://postgres:my_password@postgres:5432/my_app?encoding=utf8&pool=5&timeout=5000"
# DATABASE_URLのフォーマットは以下の通り。
プロトコル名://DBユーザー:DBパスワード@サービス(コンテナ)名:ポート番号/DB名?その他パラメーター
# config/database.yml
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
url: <%= ENV['DATABASE_URL'].gsub('?', '_development?') %>
test:
url: <%= ENV['DATABASE_URL'].gsub('?', '_test?') %>
Cucumber(コンテナではなくテストフレームワークの方)の設定ファイルはfeatures/support/env.rbとなりますが、ここで重要なのはCapybaraの設定をdocker-compose.ymlのネットワーク設定と整合性を持たせることです。
# features/support/env.rb
require 'cucumber/rails'
ActionController::Base.allow_rescue = false
begin
DatabaseCleaner.strategy = :transaction
rescue NameError
raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
end
# chromiumコンテナのchromeをdriverとして設定。
# urlはdocker-compose.ymlでのネットワーク設定と同一でなければならない。
Capybara.register_driver :selenium_chrome_headless do |app|
options = {
browser: :remote,
url: 'http://chromium:4444/wd/hub',
# clear_local_storage: true,
# clear_session_storage: true,
capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
'goog:chromeOptions': {
args: %w[
headless
disable-gpu
window-size=2048,4096
no-sandbox
disable-dev-shm-usage
]
}
)
}
Capybara::Selenium::Driver.new(app, **options)
end
Capybara.default_max_wait_time = 10
Capybara.javascript_driver = :selenium_chrome_headless
# docker-compose.ymlでのcucumberコンテナのネットワーク設定と同じにする。
Capybara.server_host = 'my_app.test'
Capybara.server_port = '41111'
Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}"
なお、Railsアプリにlocalhost以外からアクセスする場合はconfig/environments/test.rbでホスト名をホワイトリストに追加する必要があります。ここでのホスト名は、docker-compose.ymlで設定したcucumberサービスのネットワークエイリアスと同一です。
# config/environments/test.rb
Rails.application.configure do
config.hosts << 'my_app.test'
end
Chromiumコンテナ
chromiumコンテナではデフォルトでポート4444と5900がexposeされます(ホストからアクセスしたい場合はports属性を指定する必要がありますが、今回のセットアップでは不要です)。4444はwebdriver用のポートでcucumberコンテナがchromiumコンテナと通信するのに使われます。5900の使用は任意で、ブラウザーのGUIを使ってデバッグしたい時に使いますが、今回は割愛します。
shm_sizeはshared memory(共有メモリ)の略で、chromiumブラウザーがコンテナ内でクラッシュするのを防ぐために2ギガバイトを割り当てています。
dockerfile属性で指定しているDockerfileの内容は以下の通りです。
# dockerconfig/chromium/Dockerfile
FROM seleniarm/standalone-chromium
RUN sudo apt-get update -qq && sudo apt-get install -y dnsutils
テスト実行
docker composeで--profileオプションを使用してdocker-compose.ymlでe2e-testと指定したcucumberとchromiumコンテナを含む、全てのテスト用コンテナを立ち上げます(postgresコンテナはprofileを指定しなくてもデフォルトで立ち上がります)。
docker compose --profile e2e-test up -d
cucumberテストを実行します。コンテナ名とcucumber実行コマンドが同じ名前なので分かりづらいですが、1つ目がコンテナ名、2つ目が実行コマンドです。
# -iオプションはテストを実行するだけなら必要ないが、debuggerを使いたいなら必要。
docker exec -it cucumber cucumber
なお、もしRailsアプリケーションでschema.rbではなくstructure.sqlを使っている場合、そのままだとcucumberコンテナにpsqlが未インストールのエラーとなってしまいます。解決方法としてはホストでrails db:test:load_structureを実行してからテスト実行するとうまくいきます。
また、ネットワーク系のトラブルでコンテナ間で通信ができない場合は、一度コンテナにログインして確認することをお勧めします。
例えば、chromiumからcucumberコンテナにアクセスできるかは以下のように確認できます。
# cucumberコンテナにログイン
docker exec -it cucumber bash
# railsサーバーを起動し、tcp://0.0.0.0:41111をlisten
RAILS_ENV=test rails server -b 0.0.0.0 -p 41111
# 別ターミナルでchromiumコンテナにログイン
docker exec -it chromium bash
# curlでcucumberコンテナのrailsサーバーにリクエストを送信
curl my_app.test:41111
テストが無事に終了したら、コンテナをダウンします。
docker compose --profile e2e-test down
なお、もしテスト実行がうまくいかなかったりしてソースコードやDockerfileを変更した場合はコンテナを再ビルドする必要があります。全てのコンテナを再ビルドする必要はないので、必要なコンテナのみ以下のコマンドで指定します。
docker compose --profile e2e-test up --build cucumber
まとめ
この記事では、Docker を使って Rails アプリケーションの E2E テストを実行する方法について解説しました。Docker を使うことで、開発環境による差異を吸収し、安定したテスト実行環境を構築することができます。
この方法を応用することで、他の言語やフレームワークでも同様の E2E テスト環境を構築することができます。ぜひ試してみてください。
