Playwright + Github Actions で CI上の local Web 環境に E2Eテストを実行する例 ( フロント + バックエンドAPI構成 )

# 課題

Github Actions で Playwright の E2E テストを実行するにはどうすれば良いか？

対象環境はGithub Actions 上の local 環境とし、毎回データベースはリセットして、実行単位ごとに依存しないテストがしたい

# 概要


たとえばサービスがフロント+バックエンドAPI構成の場合、特に変わったことをする必要はなく、以下の手順で実現できる

- データベースサーバーを起動する
- 初期データベースや初期データ ( seed ) を作成する
- バックエンドAPIサーバーをバックグラウンド起動する
- フロントをバックグラウンド起動する
- 起動済みのフロント ( localhost ) に対してPlaywright を実行する

起動したサービス同士は すべて localhost  ( 127.0.0.1 ) で接続し合うことが出来る


# Github Actions Workflow の例

- 1個のジョブ ( jobs ) を使って直列的にワークフローを実行する。( 自分が試した中だとジョブを複数個に分けると環境が分離してしまい、複数のサーバーを起動させることができなかったため ) 
- APIサーバーもフロントも、それぞれバックグラウンド起動するようにしておく。なぜならそうしないとサーバーが起動した時点でテストが止まってしまい、後続のPlaywright実行までたどり着かないから。
- サーバー起動が完了する前にテストを開始してしまうとうまくいかないので、サーバーのレスポンスを待つ処理も入れている。
- E2Eテストは処理を順番に直列実行しなければいけないテストケースが多いと思うが、Playwrightはテストケースのファイルを複数に分けると並列実行されてしまう。このためPlaywrightコマンドの実行自体を複数回に分けて、指定のファイル順通りに順次実行するようにしている。


```yml
name: Playwright Test

on: [pull_request]

jobs:
  playwright-test:
    runs-on: ubuntu-latest
    services:
      # データベースサーバーの起動
      mysql:
        image: mysql:x.x.x
        ports:
          - 3306:3306
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
        options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup ruby for Backend API
        uses: ruby/setup-ruby@v1
        with:
          working-directory: rails-dir
          ruby-version: .ruby-version
          bundler-cache: true

      # 何らかの方法でテストに必要なデータベースとその初期データを作成する
      # この例ではRailsのマイグレーションとseedを使う
      - name: Create Database
        run: |
          bin/rails db:create
          bin/rails db:migrate
          bin/rails db:seed

      # バックエンドAPIを起動
      # 以下のように サーバーの実行コマンド自体にバックグラウンド起動モードがある場合は、それを使う ( rails -d )
      - name: Start Backend API On Background
        run: |
          bin/rails server -d -p 4000

      # APIの起動を待ち受ける
      - name: Wait Backend API Response
        run: |
          curl --retry 5 --max-time 5 http://localhost:4000

      - name: Setup node for Front 
        uses: actions/setup-node@v4
        with:
          node-version-file: '.node-version'
          cache: 'npm'
          cache-dependency-path: 'package-lock.json'

      - name: Install dependencies for Front
        run: npm install

      # フロントを起動
      # 実行するコマンド本体にバックグランド起動モードがない場合、以下のように Linux コマンドの機能でバックグラウンド実行する
      - name: Start Front on Background
        run: |
          npm run build
          nohup npm run start &

      # フロントの起動を待ち受ける
      - name: Wait Front Response
        run: |
          curl --retry 5 --max-time 5 http://localhost:3000

      - name: Install Playwright Browsers
        run: npx playwright install chromium

      # Playwrightテストを実行する
      - name: Playwright Tests
        run: |
          npx playwright test flow_A_step_1.spec.ts
          npx playwright test flow_A_step_2.spec.ts
          npx playwright test flow_B.spec.ts
          npx playwright test flow_C.spec.ts

```

API から データベースの接続も localhost を設定すれば良い
たとえばRailsのdatabase設定であれば以下のように

```yml
test:
  adapter: mysql2
  username: root
  password:
  host: 127.0.0.1
  database: end_to_end_test
```

# 補足: モックの課題

Playwright にはHTTPレスポンスの結果をモックできる関数 ( fullfill ) があり有用だが、モックを使うほど実環境のテストから乖離してしまう。あとはモックの値自体の管理も大変だ。

このためHTTPレスポンスをモックしないテストが出来る方が良い。
ただ純粋なE2Eはテストケースの組み方自体にコツが必要なので、そこは対策しなければいけない。

# E2E のテストケースの組み方

ちゃんとしたE2Eテストをする場合、モックを使うよりもテストケースの組み方自体は難しくなる。

なぜなら1個のテストケースが実環境のデータを変えてしまうので、後続のテストケースはそれを考慮した作りにしておく必要があるからだ。
(E2Eテストなのでそれが正しい)

以下が対策となるだろう。

- A. 全てのテストケースを順次実行した時に成り立つようにテストケースを組んでおく。
- B. もしくは、1個ずつのテストケースは、なるべく環境がどんなデータ状態でも成り立つように作っておく。
- C. もしくは、テストケースとテストケースの実行の間にデータベースのリセット処理を挟む。(だがこれはCI上では成り立っても、実環境では成り立たないので、やむを得ない場合の対策だ )


# Playwrightの設定でWebサーバーを起動する

https://playwright.dev/docs/test-webserver

Workflow上でWebサーバーを起動しなくても、Playwrightの config に設定することも出来るようだが、 playwright コマンド自体を複数回に分ける上の例では、サーバー起動も複数回に分かれてしまい、時間がかかってしまう

なのであくまでworkflow上でWebサーバーを起動した方が良さそうだ

もしこの機能を使う場合は標準出力を有効にしておくと実行結果が分かりやすくなるだろう


```ts
export default defineConfig({
  // Run your local dev server before starting the tests
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
    stdout: 'pipe',
    stderr: 'pipe',
  },
});
```

これを設定した場合は、上の例のWebサーバー起動のバックグランド実行や、Webのレスポンスを待ち受ける処理は必要なくなりそう

# チャットメンバー募集


何か質問、悩み事、相談などあればLINEオープンチャットもご利用ください。

https://line.me/ti/g2/eEPltQ6Tzh3pYAZV8JXKZqc7PJ6L0rpm573dcQ


# プロフィール・経歴

https://github.com/YumaInaura/YumaInaura