ITエンジニアによるITエンジニアのためのブログ

DockerでRailsのE2Eテスト実行

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コンテナ間ネットワークをイメージすると以下の画像のようになります(数字はポート番号です)。

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 テスト環境を構築することができます。ぜひ試してみてください。