Playwright+GitHub Actions*E2E with VRT 環境構築とCI/CD連携の知見
はじめに
業務でPlaywrightの環境構築及びCI/CD連携担当したことから、E2EテストとVRTのベストな構成をずっと悩んでいました。
自分の中である程度納得できる形まで落とし込めたので、その知見を残しておきます。
🎭Playwrigth
Microsoftが開発したテストツールです。複数ブラウザ対応、自動待機機能、並列処理などによりE2Eテストを実施します。また、スクリーンショットの比較によるテスト(VRT:Visual Regression Testing)により視覚的変更も検出可能です。元々Puppeteerを作っていたチームにより開発が行われているようです。
ツールの比較対象にcypressがありますが、最近の週間ダウンロード数はPlaywrightが上回っているようです。
(他にはSeleniumやベンダー提供の有料E2Eテストツールなどもあります)
また、PlaywrightはJava、Python、C#の言語も公式にサポートしていますが、今回はTypeScriptを使用します。
要件
まず、今回のリポジトリ内にはserver(Express.js)とweb(React.js)の2つのパッケージがあり、開発環境はdocker compose up
により起動します。 DBもdocker-compose.ymlで定義します。
- GitHub ActionsでE2Eテストが成功する
- GitHub ActionsでVRTが成功する
- ローカルとCI環境で同一の結果を再現できる
当記事ではE2EとVRTを以下と定義します
- E2E(End-to-End Testing)...システム全体を通して機能が正しく動作するかを確認するテスト
- VRT(Visual Regression Testing)...視覚的な変更を検出するテスト
Source
今回のソースコードはこちらになります。
当リポジトリのGitHub Actionsの実行履歴です。毎朝9時にE2EテストとVRTを日時実行しています。
全体構成
/playwright-e2e-stack
├── /.github/workflows
│ ├── e2e.yml
│ └── vrt.yml
├── /e2e
│ ├── /tests # E2Eテストケース
│ ├── Dockerfile # for E2Eテスト実行環境
│ └── playwright.config.ts # E2Eテスト設定
├── /server
│ └── Dockerfile
├── /web
│ ├── /web/tests/visual/mock/db.json # json-serverに使うモックデータ
│ ├── /tests # VRTテストケース
│ ├── Dockerfile
│ ├── Dockerfile.test # for VRT実行環境
│ ├── playwright.config.ts # VRT設定
│ └── docker-compose.vrt.yml
├── docker-compose.yml
└── docker-compose.e2e.yml
E2EテストとVRTのディレクトリ構成の考え方
-
E2Eテスト:
システム全体を通して行うテストであるため、serverやwebと同レベルの階層にe2eディレクトリを作成しています。これにより:- ルートディレクトリに配置することで、システム全体を通してテストということが明確になる
- serverとwebの両方を考慮したテスト設計が行いやすい
-
VRT:
UIの視覚的な変更を検出するテストであり、フロントエンドに特化しています。そのため:- web配下にテスト定義を配置することで、UI変更との関連性が明確になる
- フロントエンド開発者が直接管理しやすい
- APIモックを使用することで、バックエンドに依存せずテストが可能
E2E(End-to-End Testing)詳細
要点
docker compose -f docker-compose.e2e.yml up
でE2Eテストを実行します。
services:
db:
image: postgres:latest
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
ports:
- "5432:5432"
healthcheck:
test: pg_isready -U admin -d mydb
interval: 3s
timeout: 3s
retries: 5
server:
build:
context: ./server
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://admin:password@db:5432/mydb?schema=public
- WEB_ORIGIN=http://web
ports:
- "3000:3000"
command: >
sh -c "npx prisma migrate deploy && npm start"
depends_on:
db:
condition: service_healthy
web:
build:
context: ./web
dockerfile: Dockerfile
args:
- VITE_API_URL=http://server:3000
ports:
- "80:80"
e2e:
build:
context: ./
dockerfile: ./e2e/Dockerfile
environment:
- CI=${CI}
- DATABASE_URL=postgresql://admin:password@db:5432/mydb?schema=public
- PLAYWRIGHT_BASE_URL=http://web
- PLAYWRIGHT_API_URL=http://server:3000
volumes:
- ./e2e/test-results:/app/test-results
ports:
- "9323:9323"
depends_on:
- web
以下の順番で処理が行われていきます。
-
DBコンテナの起動
- Postgresデータベースを起動
- ヘルスチェックを実行し、データベースの準備ができたことを確認
-
サーバーコンテナの起動
- DBコンテナのヘルスチェックが成功するのを待つ
- サーバーアプリケーションをビルド
- Prismaマイグレーションを実行
- サーバーアプリケーションを起動
-
Webコンテナの起動
- フロントエンドアプリケーションをビルド
- Webサーバーを起動
-
E2Eテストコンテナの起動
- E2Eテスト環境をビルド
- Webコンテナの起動を待つ
- テスト実行の準備
-
テストの実行
- E2Eテストコンテナ内でPlaywrightテストを実行
- テスト結果をボリュームにマウントしたディレクトリに保存
FROM mcr.microsoft.com/playwright:latest
EXPOSE 9323
WORKDIR /app
# ソースコードのコピーより先に npm ci をすることで依存関係のレイヤーをキャッシュする
COPY ./e2e/package*.json .
RUN npm ci
COPY ./e2e .
COPY ./server/prisma ./prisma
RUN npx prisma generate
CMD ["npm", "test"]
テスト実行環境には、Playwright公式の「mcr.microsoft.com/playwright」をベースイメージとして使用します。公式イメージを採用することで、テスト実行に必要な依存関係が事前にセットアップを省略でき、環境構築の手間を大幅に削減できます。
また、コンテナ化されたテスト環境を使用することで、ローカル開発環境とCI環境間での実行環境の差異を最小限に抑えることができます。これにより、環境の違いによるテスト結果の不一致を防ぎ、より信頼性の高い一貫したテスト結果を得ることができます。
name: End-to-End Testing
run-name: E2E Testing By @${{ github.actor }}
on:
workflow_dispatch:
push:
branches: ["main"]
schedule:
# 毎日 9:00 (JST)
- cron: "0 0 * * *"
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
run: 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
GitHub Actionsワークフローの解説です。
-
トリガー設定:
便宜上mainブランチへのプッシュ時も設定していますが、E2Eテストはその他のテストに比べて時間がかかる場合が多いです。そのため、基本的には特定のブランチに対して日時で実行するようにします。 -
テストの実施:
docker compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from e2e
によりテストを実行します。(ローカル環境と一緒です。)
--abort-on-container-exit
により、いずれかのコンテナが終了した時点で、他の全てのコンテナも停止するようにし、--exit-code-from e2e
によりe2eコンテナの終了コードを、このコマンド全体の終了コードとして使用しています。 -
テストレポートのアップロード:
e2e/playwright-report
配下に作成されたテストレポートをアーティファクトにアップロードします。これにより、GitHub Actions上で行われたテストの詳細を確認することができます。 -
リソースのクリーンアップ:
docker compose down --rmi all --volumes --remove-orphans
により、関連リソースを削除します。不要なデータや設定を残さないことで、潜在的なセキュリティリスクを軽減します。
コマンドはこちらからお借りしました。
GitHub Actionsのテストレポート
GitHub Actionsでテスト実行後、生成された「playwright-report」をダウンロードしnpx playwright show-report [ダウンロードしたパス]
を実行すると、詳細なテスト結果や失敗時のスクリーンショットなどを確認でき、問題の特定と解決に役立ちます。
npx playwright show-report ~/Downloads/
# or
npx playwright show-report ~/Downloads/playwright-report など
また、テスト結果をダウンロードするのではなく、GitHub Pagesでホスティングすることもできるそうです。素敵ですね。
VRT(Visual Regression Testing)詳細
キャプチャどこに保存する?
まず、画像比較に使用するキャプチャをどこに保存するかですが、候補としては以下が上げられます。
- S3などのクラウドストレージ
- GitHub ActionsのArtifacts
- 実行の度に正しいキャプチャを取得する
- コミットに含める
参考
なるべく外的要因に左右されない形式にしたかったので、コミットに含めることにしました。
APIモックサーバー
APIのモックサーバーにはjson-serverを使用しています。
{
"users": [
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane.smith@example.com"
}
]
}
上記のようなjsonを用意することで簡単に都合の良いREST APIを用意することができます。
その他の要点
E2E同様にdocker compose -f ./web/docker-compose.vrt.yml up
でテストを実行します。
services:
vrt:
build:
context: ./
dockerfile: Dockerfile.test
args:
- VITE_API_URL=http://localhost:3000
environment:
- CI=${CI}
ports:
- "80:80"
- "3000:3000"
- "9323:9323"
volumes:
- ./playwright-report:/app/playwright-report
- ./test-results:/app/test-results
- ./snapshots:/app/snapshots
- ./tests/:/app/tests
VRTではwebディレクトリ配下のみを使用します。
繰り返しになりますが、VRTの関心はフロントエンドです。(プロジェクトによって考え方違うかもですが…)そのため、すべてweb配下で完結する構成にしました。
FROM mcr.microsoft.com/playwright:latest
EXPOSE 80
WORKDIR /app
COPY package*.json .
RUN npm ci
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
COPY . .
RUN npm run build
CMD ["npm", "test"]
E2Eと同様の理由でPlaywright公式のイメージを使用しています。
とくにVRTでは(私の手元の)Macで取得したキャプチャとGitHub Actions上で取得したキャプチャでは端末差異が顕著に現れます。これを回避するため、コンテナ化されたテスト環境は必須になりそうです。
webServer: [
{
command: "npm start",
url: 'http://localhost',
reuseExistingServer: !process.env.CI,
},
{
command: "npm run mock",
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
],
E2Eではサーバーの立ち上げはすべてdocker-compose.ymlで管理していましたが、VRTではplaywright.config.ts
のwebServerフィールドを使用して、テストの実行時に必要なサーバーを起動しています。
name: Visual Regression Testing
run-name: VRT By @${{ github.actor }}
on:
workflow_dispatch:
push:
branches: ["main"]
schedule:
# 毎日 9:00 (JST)
- cron: "0 0 * * *"
jobs:
vrt:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: docker compose build
run: docker compose -f ./web/docker-compose.vrt.yml build
- name: Run Playwright tests
run: docker compose -f ./web/docker-compose.vrt.yml up --abort-on-container-exit --exit-code-from vrt
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: web/playwright-report/
retention-days: 30
- name: Cleanup Docker resources
if: always()
run: docker compose -f ./web/docker-compose.vrt.yml down --rmi all --volumes --remove-orphans
ワークフローはディレクトリやdocker-compose.ymlの指定以外はE2Eと同じです。
まとめ
本記事では、Playwrightを使用したE2EテストとVRTの実装について解説しました。ポイントをまとめます。
-
環境構築:
- docker-composeを使用し、ローカル環境とCI環境で一貫したテスト環境を構築
- Playwright公式イメージを使用することで、環境依存を抑制
-
E2Eテスト:
- システム全体を対象とし、実際のバックエンドと連携したテストを実現
- ルートディレクトリにe2eディレクトリを配置することで、システム全体に対してのテストであることを明確化
-
VRT(Visual Regression Testing):
- フロントエンドに特化し、UIの視覚的変更を検出
- モックサーバーを使用することでフロントエンドのみのテストを可能に
-
CI/CD連携:
- GitHub Actionsを使用し、自動テスト実行とレポート生成を実現
- 特定のブランチに対して日時実行することで継続的な品質保証を実現
まず、VRTについてです。当初私はVRTとE2Eテストを同様のスコープに対して行おうとしており、環境の共通化などでとても悩まされました。その後考えをあらため、それぞれの関心事を整理することで、ある程度はシンプルな構成にできたと思っています。(つまずいたポイント②です)
また、docker-compose周りも「なんとなく」使っていた箇所が多いことが自覚でき、この機会に学び直すことができました。よかったらどうぞです。笑
E2Eテストは導入がしやすく、システム全体を通して検証が行えるコスパの良いテストだと思います。ですが、その反面メンテナンスコストも高いです。E2Eテストを皮切りに統合テストや単体テストを怠らず充実させていきたいですね!
最後に、いろいろアドバイスくれた方々、ありがとうございました!
おまけ
つまずいたポイント①
初めはdocker-composeではserverとdbのみを管理し、webはローカル環境(ホストマシン)で立ち上げる構成を考えていました。
この場合、ローカル環境ではlocalhostを通してweb→serverの疏通が行えるのですが、GitHub Actions上ではそれが失敗します。
記事にある通り、webもdocker-composeで管理することでDocker Networkを通して相互に疎通で可能になります。
つまずいたポイント②
初めはVRTもE2Eの一種と捉え、どちらもe2eディレクトリで管理していました。
共通化されたdocker-compose.yml
やplaywright.config.ts
は可読性や変更容易性がとても低い実装になりました。
今回はe2e配下にE2Eを、web配下にVRTを定義することでそれぞれを完全に別けています。
End to Endで行うVRTもあると思います。そのときは上記のように無理した共通化はせず、それぞれの環境を構築するのがおすすめです。
つまずいたポイント③
プロジェクトによっては、docker-composeでdepends_on
やhealthcheck
を使用しても、サービスの起動順序を制御しきれない場合がありました。その場合wait-for-itや、dockerizeを使用することでコントロールすることができます。
公式でも紹介されています。
Discussion