Docker上のPlaywrightから、Docker上のNext.jsをe2eテストする
これは何?
- Dockerに構築したPlaywrightから、Dockerに構築したNext.jsをe2eテストする設定を行った
- GitHub Actions による CI, Playwright UI test もできるようにした
- 上記設定を、備忘録として残す
構築環境
- Next.js環境
- Next.js 15.3.1
- Prisma 6.3.1
- Auth.js 5.0.0-beta.25
- Postgres 16.4
- パッケージマネージャ
- bun
- Dockerイメージ
- oven/bun:latest
- Playwright環境
- Playwright v15.2
- パッケージマネージャ
- npm
- Dockerイメージ
- mcr.microsoft.com/playwright:v1.52.0-noble
- ローカル環境
- Windows 11(WSL2)
- Docker Compose v2.32.4-desktop
- CI
- GitHub Actions
モチベーションとメリット
- Dockerでe2eを動かすことで、ローカルとCIとの差異を無くしたい
- CIの設定を簡素化したい
- Docker環境でも、なるべくパフォーマンスを落とさずにホットリロードしたい
デメリット
- 開発環境が複雑になった
- 依存関係が生まれ、管理が複雑になった
- コンテナを立ち上げてからファイルを編集しないと、編集結果がリアルタイムにコンテナに反映されない
- Next.jsは
bun
、Playwrightはnpm
という二重管理になった。- PlaywrightはDockerで動かす際にmicrosoftのDockerイメージを使うが、npmの利用が前提になっている
- bunでPlaywrightを実行すると、Playwrightは不安定になると報告がある
- PlaywrightはDockerで動かす際にmicrosoftのDockerイメージを使うが、npmの利用が前提になっている
- 依存関係が生まれ、管理が複雑になった
- 開発環境の初回立ち上げが遅い
- テスト環境と開発環境で同じDockerfileを使い、テストをなるべく本番に似せた環境で実行したいために
next build
を実施している。そのため、開発環境の初回立ち上げ時にはビルドが走る。結果として、開発環境のコンテナを初回起動させるためには140秒程度の時間がかかっている。
- テスト環境と開発環境で同じDockerfileを使い、テストをなるべく本番に似せた環境で実行したいために
開発体験が損なわれるのは割と大きなデメリットなので、正直なんとかしたいところ。しかし初回起動でなければコンテナの起動時間は10秒以内に収まるので、依存関係の煩わしさと引き換えに妥協した。改善の余地はある(最後のおまけに記述)。
環境構築
ディレクトリ構成
最終的なディレクトリ構成は以下のとおり(主要なものだけ記載)。
npmとbunで違うpackage.jsonを使うのが特徴。
e2eと同じようにNext.js用の環境もapplication
のようなディレクトリを切って管理したほうが綺麗な構造かもしれないが、今はやっていない。
nextjs-joke # リポジトリのルート(プロジェクトルート)
├── /.github/workflows
│ └── e2e.yml
├── /e2e
│ ├── package.json # npmで管理
│ ├── package-lock.json
│ ├── Dockerfile # Playwright用
│ ├── .dockerignore
│ └── playwright.config.ts
├── package.json # bunで管理
├── bun.lockb # bunが出力するパッケージ管理ファイル
├── Dockerfile # Next.js用
├── .dockerignore
├── docker-compose.yml # 開発環境用
└── docker-compose.e2e.yml # e2e用
リポジトリを直接見たい方はこちら:
Next.js
過去記事の通り:
Playwright
package.json
e2e用の独立したファイルとした。
{
"name": "e2e",
"version": "1.0.0",
"scripts": {
"test": "playwright test",
"test-ui": "playwright test --ui-host=0.0.0.0 --ui-port=9323"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@prisma/client": "^6.6.0",
"@types/node": "22.15.3"
}
}
パッケージインストールするときは、プロジェクトルートから以下のように実行:
$ npm --prefix ./e2e install ./e2e
Dockerfile
# 公式イメージ。依存関係を含むパッケージがデフォルトでインストールされている
# このイメージにはplaywright自体は入ってないので別途インストールする必要がある
FROM mcr.microsoft.com/playwright:v1.52.0-noble
EXPOSE 9323
WORKDIR /app
# playwrightのインストール
COPY ./package*.json .
RUN npm ci
# e2e/配下の残りのファイルをホスト側からコピー
COPY . .
# ホスト側のプロジェクトルートから、必要なファイルをコピー
# commonの設定はe2e/docker-compose.ymlに記載
# commonはプロジェクトルートを指定
COPY ./tsconfig.json .
COPY ./prisma prisma/
COPY ./prisma.ts .
# テストデータ作成のためにprisma clientを設定
RUN npx prisma generate
COPY --from=common
がミソ。
docker-compose.e2e.yml
volumes:
pg-data:
services:
e2e-postgres:
image: postgres:16.8-alpine3.21
volumes:
- pg-data:/var/lib/postgresql/data
ports:
- "5433:5433"
environment:
- POSTGRES_DATABASE=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_ROOT_PASSWORD=root
- PGPORT=5433
healthcheck:
test: pg_isready -U postgres -d postgres
interval: 3s
timeout: 3s
retries: 5
e2e-application:
build:
context: '.' # プロジェクトルートから読み込むことを指定
dockerfile: Dockerfile
ports:
- "3001:3001"
command: >
sh -c "bunx prisma migrate deploy && bun start"
user: vscode
environment:
- POSTGRES_PRISMA_URL=postgres://postgres:password@e2e-postgres:5433/postgres?pgbouncer=true&connect_timeout=15
- POSTGRES_URL_NON_POOLING=postgres://postgres:password@e2e-postgres:5433/postgres
- APP_ENV=test
- AUTH_TRUST_HOST=http://e2e-application:3001
- PORT=3001
- AUTH_SECRET=test-dummy
develop:
watch:
- action: sync
path: .
target: /app
- action: rebuild
path: ./package.json
depends_on:
e2e-postgres:
condition: service_healthy
e2e:
build:
context: './e2e' # プロジェクトルート/e2e から読み込むことを指定
dockerfile: Dockerfile
# contextをプロジェクトルート/e2e にしたので、dockerはこのcontext配下のファイルしか
# 読めない設定となったが、additional_contextsを指定することで特定のディレクトリを
# 読み込み対象とすることができる。ここでは、プロジェクトルート配下のファイルを読みたい
# のでプロジェクトルートをcommonとしてコンテキストを追加した。
# 実際の使い方はe2e/Dockerfileを参照
additional_contexts:
- common=./
environment:
- CI=${CI}
# Docker同士の通信では、サービス名をホスト名に指定する
- PLAYWRIGHT_BASE_URL=http://e2e-application:3001
- POSTGRES_PRISMA_URL=postgres://postgres:password@e2e-postgres:5433/postgres?pgbouncer=true&connect_timeout=15
- POSTGRES_URL_NON_POOLING=postgres://postgres:password@e2e-postgres:5433/postgres
- APPDOMAIN=e2e-application
# CIとローカルで起動するコマンドを分けたいので、環境変数をコマンドラインに渡して
j# 判定させる
command: >
sh -c "${E2E_SERVICE_COMMAND}"
volumes:
- ./e2e/playwright-report:/app/playwright-report
- ./e2e/test-results:/app/test-results
ports:
- "9323:9323"
develop:
watch:
# ホスト側の./e2eディレクトリ配下のファイルが変更されたら、コンテナをホットリロード
- action: sync
path: ./e2e
target: /app
# ホスト側の./e2e/package.jsonが変更されたら、Dockerイメージをリビルド
- action: rebuild
path: ./e2e/package.json
depends_on:
- e2e-application
特徴は、contextを./e2eに指定して余計なファイルをDockerに読ませないようにしたこと。
ただ、そうするとプロジェクトルート配下のファイルが読まれないのでadditional_contextsを指定。
ちなみに、プロジェクトルートをコンテキストにすると、dockerfileの指定が./e2e/Dockerfile
であっても、プロジェクトルート配下の.dockerignoreが読まれてしまい、意図しない挙動となる可能性があるので注意。(dockerの動きをあまり理解していなくてここで2時間くらい溶かした)。
watch
の設定を入れている理由としては、テストコードを修正したらすぐ再テストしたいから。また、関連してアプリケーションコードも弄りたいだろうからe2e-application
にもwatch
の設定を入れてホットリロードを可能にした。
.github/workflows/e2e.yml
こちらは参考元の記事を殆どそのまま利用。
起動単位はpush
にしたが、e2eは時間のかかるテストなので検討の余地あり。
name: End-to-End Testing
run-name: E2E Testing By @${{ github.actor }}
on: [push] # コードがpushされるごとに実行
jobs:
e2e:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: docker compose build
run: docker compose -f docker-compose.e2e.yml build
- name: Run Playwright tests
# E2E_SERVICE_COMMANDにnpm testを渡し、非UIモードでテスト
run: E2E_SERVICE_COMMAND='npm test' docker compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 30
- name: Cleanup Docker resources
if: always()
run: docker compose -f docker-compose.e2e.yml down --rmi all --volumes --remove-orphans
npm test
でpackage.jsonに記載したplaywright test
が動く。なお、npm test
は特殊なコマンドで、test
以外を実行しようとする場合にnpm run コマンド名
と記載する必要があるのを忘れてハマった(bun
だと基本的にbun コマンド名
で動くのでごっちゃになった)。
ref. https://qiita.com/kikkaya088/items/65cf41a826bbb9513702
動作確認
ローカルでUIモードでテスト
$ E2E_SERVICE_COMMAND='npm run test-ui' docker compose -f docker-compose.e2e.yml up --build --watch
package.jsonに記載したplaywright test --ui-host=0.0.0.0 --ui-port=9323
が起動し、UIモードでテストできる。
ブラウザにlocalhost:9323
とアクセスすると以下のようになる。
※ Docker上で起動したplaywrightをブラウザで表示させたいときはui-host
とui-port
の指定が必要。
ref. https://playwright.dev/docs/docker#remote-connection
--watch
とつけているので、アプリケーションコードやテストコードを修正したらホットリロードされる(ただ、プロジェクトルートからコピーしたtsconfig.json
などを修正しても自動反映されないので、Dockerイメージをリビルドしないといけない)
CI
CIが正常終了し、結果レポートもアップロードされている。
開発環境の立ち上げ
docker-compose.e2e.ymlの立ち上げと混同しがちなので、--build
は常につけると混乱しない。
$ docker compose up --build --watch
終わりに
参考とした記事がとてもよかった。
開発体験の向上が今後の課題だが、個人開発で使うのであれば充分に捗りそうな予感がしている。
VRTも試してみたい。
e2eを充実させていきたい。
おまけ
開発環境の立ち上げを高速にしたい場合、docker compose
の--build-arg
オプションで可能だった。
まず、DockerfileのRUNの記述にARG
で変数を指定し、デフォルト値としてアプリケーションのビルドコマンドを設定しておく。
ARG APP_BUILD_CMD='bun deployable-test'
RUN bunx prisma generate \
&& ${APP_BUILD_CMD}
次に、--build-arg
オプションで指定した変数に空文字を渡せばアプリケーションのビルドコマンドが実行されなくなる。
docker compose build --build-arg APP_BUILD_CMD='' && docker compose up --watch
これで立ち上げが60秒くらい早くなる(ただ、アプリケーション全体の動きが多少もっさりする)。
Discussion