🎭

Playwright+GitHub Actions*E2E with VRT 環境構築とCI/CD連携の知見

2024/07/21に公開

はじめに

業務でPlaywrightの環境構築及びCI/CD連携担当したことから、E2EテストとVRTのベストな構成をずっと悩んでいました。
自分の中である程度納得できる形まで落とし込めたので、その知見を残しておきます。

🎭Playwrigth

Microsoftが開発したテストツールです。複数ブラウザ対応、自動待機機能、並列処理などによりE2Eテストを実施します。また、スクリーンショットの比較によるテスト(VRT:Visual Regression Testing)により視覚的変更も検出可能です。元々Puppeteerを作っていたチームにより開発が行われているようです。

https://playwright.dev/

ツールの比較対象にcypressがありますが、最近の週間ダウンロード数はPlaywrightが上回っているようです。
(他にはSeleniumやベンダー提供の有料E2Eテストツールなどもあります)

また、PlaywrightはJava、Python、C#の言語も公式にサポートしていますが、今回はTypeScriptを使用します。

要件

まず、今回のリポジトリ内にはserver(Express.js)とweb(React.js)の2つのパッケージがあり、開発環境はdocker compose upにより起動します。 DBもdocker-compose.ymlで定義します。

  1. GitHub ActionsでE2Eテストが成功する
  2. GitHub ActionsでVRTが成功する
  3. ローカルとCI環境で同一の結果を再現できる

当記事ではE2EとVRTを以下と定義します

  • E2E(End-to-End Testing)...システム全体を通して機能が正しく動作するかを確認するテスト
  • VRT(Visual Regression Testing)...視覚的な変更を検出するテスト

Source

今回のソースコードはこちらになります。

https://github.com/ishiyama0530/playwright-e2e-stack

当リポジトリのGitHub Actionsの実行履歴です。毎朝9時にE2EテストとVRTを日時実行しています。

https://github.com/ishiyama0530/playwright-e2e-stack/actions

全体構成

/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のディレクトリ構成の考え方

  1. E2Eテスト:
    システム全体を通して行うテストであるため、serverやwebと同レベルの階層にe2eディレクトリを作成しています。これにより:

    • ルートディレクトリに配置することで、システム全体を通してテストということが明確になる
    • serverとwebの両方を考慮したテスト設計が行いやすい
  2. VRT:
    UIの視覚的な変更を検出するテストであり、フロントエンドに特化しています。そのため:

    • web配下にテスト定義を配置することで、UI変更との関連性が明確になる
    • フロントエンド開発者が直接管理しやすい
    • APIモックを使用することで、バックエンドに依存せずテストが可能

E2E(End-to-End Testing)詳細

要点

docker compose -f docker-compose.e2e.yml upでE2Eテストを実行します。

docker-compose.e2e.yml
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

以下の順番で処理が行われていきます。

  1. DBコンテナの起動

    • Postgresデータベースを起動
    • ヘルスチェックを実行し、データベースの準備ができたことを確認
  2. サーバーコンテナの起動

    • DBコンテナのヘルスチェックが成功するのを待つ
    • サーバーアプリケーションをビルド
    • Prismaマイグレーションを実行
    • サーバーアプリケーションを起動
  3. Webコンテナの起動

    • フロントエンドアプリケーションをビルド
    • Webサーバーを起動
  4. E2Eテストコンテナの起動

    • E2Eテスト環境をビルド
    • Webコンテナの起動を待つ
    • テスト実行の準備
  5. テストの実行

    • E2Eテストコンテナ内でPlaywrightテストを実行
    • テスト結果をボリュームにマウントしたディレクトリに保存
e2e/Dockerfile
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環境間での実行環境の差異を最小限に抑えることができます。これにより、環境の違いによるテスト結果の不一致を防ぎ、より信頼性の高い一貫したテスト結果を得ることができます。

.github/workflows/e2e.yml
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により、関連リソースを削除します。不要なデータや設定を残さないことで、潜在的なセキュリティリスクを軽減します。

コマンドはこちらからお借りしました。
https://qiita.com/suin/items/19d65e191b96a0079417

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でホスティングすることもできるそうです。素敵ですね。

https://ysfaran.github.io/blog/2022/09/02/playwright-gh-action-gh-pages/#publish-html-report-to-github-pages

VRT(Visual Regression Testing)詳細

キャプチャどこに保存する?

まず、画像比較に使用するキャプチャをどこに保存するかですが、候補としては以下が上げられます。

  • S3などのクラウドストレージ
  • GitHub ActionsのArtifacts
  • 実行の度に正しいキャプチャを取得する
  • コミットに含める

参考
https://zenn.dev/cybozu_ept/articles/practice-vrt-using-github-actions-cache#vrt-導入の課題-〜テストオラクルたるスクリーンショットをどこに保管する?〜

なるべく外的要因に左右されない形式にしたかったので、コミットに含めることにしました。

APIモックサーバー

APIのモックサーバーにはjson-serverを使用しています。

web/tests/visual/mock/db.json
{
    "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でテストを実行します。

web/docker-compose.vrt.yml
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配下で完結する構成にしました。

web/Dockerfile.test
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上で取得したキャプチャでは端末差異が顕著に現れます。これを回避するため、コンテナ化されたテスト環境は必須になりそうです。

web/playwright.config.ts
  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フィールドを使用して、テストの実行時に必要なサーバーを起動しています。

.github/workflows/vrt.yml
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の実装について解説しました。ポイントをまとめます。

  1. 環境構築:

    • docker-composeを使用し、ローカル環境とCI環境で一貫したテスト環境を構築
    • Playwright公式イメージを使用することで、環境依存を抑制
  2. E2Eテスト:

    • システム全体を対象とし、実際のバックエンドと連携したテストを実現
    • ルートディレクトリにe2eディレクトリを配置することで、システム全体に対してのテストであることを明確化
  3. VRT(Visual Regression Testing):

    • フロントエンドに特化し、UIの視覚的変更を検出
    • モックサーバーを使用することでフロントエンドのみのテストを可能に
  4. CI/CD連携:

    • GitHub Actionsを使用し、自動テスト実行とレポート生成を実現
    • 特定のブランチに対して日時実行することで継続的な品質保証を実現

まず、VRTについてです。当初私はVRTとE2Eテストを同様のスコープに対して行おうとしており、環境の共通化などでとても悩まされました。その後考えをあらため、それぞれの関心事を整理することで、ある程度はシンプルな構成にできたと思っています。(つまずいたポイント②です)
また、docker-compose周りも「なんとなく」使っていた箇所が多いことが自覚でき、この機会に学び直すことができました。よかったらどうぞです。笑

https://zenn.dev/ishiyama/scraps/d91448f8d3d6bf

E2Eテストは導入がしやすく、システム全体を通して検証が行えるコスパの良いテストだと思います。ですが、その反面メンテナンスコストも高いです。E2Eテストを皮切りに統合テストや単体テストを怠らず充実させていきたいですね!

最後に、いろいろアドバイスくれた方々、ありがとうございました!

おまけ

つまずいたポイント①

初めはdocker-composeではserverとdbのみを管理し、webはローカル環境(ホストマシン)で立ち上げる構成を考えていました。

この場合、ローカル環境ではlocalhostを通してweb→serverの疏通が行えるのですが、GitHub Actions上ではそれが失敗します。

https://blog.ojisan.io/container-test-on-gha/

記事にある通り、webもdocker-composeで管理することでDocker Networkを通して相互に疎通で可能になります。

つまずいたポイント②

初めはVRTもE2Eの一種と捉え、どちらもe2eディレクトリで管理していました。
共通化されたdocker-compose.ymlplaywright.config.tsは可読性や変更容易性がとても低い実装になりました。
今回はe2e配下にE2Eを、web配下にVRTを定義することでそれぞれを完全に別けています。

End to Endで行うVRTもあると思います。そのときは上記のように無理した共通化はせず、それぞれの環境を構築するのがおすすめです。

つまずいたポイント③

プロジェクトによっては、docker-composeでdepends_onhealthcheckを使用しても、サービスの起動順序を制御しきれない場合がありました。その場合wait-for-itや、dockerizeを使用することでコントロールすることができます。

公式でも紹介されています。
https://docs.docker.jp/compose/startup-order.html

Discussion