🎬

Next.js,PrismaのE2EテストをGitHub Actionsで自動化

2024/12/17に公開

はじめに

こんにちは。株式会社ミラボの開発部に所属している川原です。
今回はNext.js(SSR)とPrismaで構築したWebシステムのE2Eテストをご紹介します。

背景

毎回リリース時に既存機能が正常に動作しているか不安で、主要機能についてはブラウザで同じような動作確認を繰り返していました。
これでは時間と労力もかかり確認漏れも発生する恐れもあるため、実際の利用者操作を自動化し確認できるようにしたいと考え、E2Eテスト作成し自動化することにしました。

最終目標

  • Pull Request作成時に、E2Eテストが自動実行されること
  • 毎回同じ環境でテストできるように、実行前にテスト用のデータベースを起動・初期化すること
  • 利用者操作→管理者操作の順で実際の運用フローに沿った試験が行えること
  • 最小限のシナリオにとどめ、実行時間は必ず5分以内で終わる操作パターンに絞り記述すること

この記事を読んで欲しい人

  • GitHub Actionを利用してE2Eテストを自動化したい方
  • Next.js,PrismaでE2Eテストの実装を考えている方
  • Playwrightを使ってE2Eテストを行いたい方

開発環境

  • MacBook Pro M1
  • VS Code
  • OrbStack

ライブラリ

  • Next.js: 14.2.15
  • Prisma: 5.20.0
  • Playwright: 1.48.0

やったこと

  1. Playwrightのインストール
  2. Playwrightのconfigファイルの設定
  3. Dockerでテスト用データベースの作成
  4. PrismaのSeedingを利用した初期データの準備
  5. シナリオテスト用のテストコード作成
  6. GitHub Actionsで自動化
    ※Next.js,Prismaの環境構築については、本記事では触れません。

1.Playwrightのインストール

まず初めにPlaywrightをインストールします。

pnpm create playwright

コマンド実行後には、下記の3つの対話が求められます。

  • テストフォルダー名
  • GitHub Actionsのworkflowファイルを雛形を作成するか
  • Playwrightで使用するブラウザをインストールするか

実行後、GitHub Actions実行用のymlファイル.github/workflows/playwright.ymlも作成され、導入がすごく簡単です。

2.Playwrightのconfigファイルの設定

今回は、シナリオテストを目的としており、「利用者の操作」 → 「管理者の操作」の流れで、直列処理でテストを行うため、workers1fullyParallelfalseに設定し、ブラウザについては「chromium」のみとしました。

各待機時間の設定できますが、timeoutの設定が短すぎるとコードは正しいけれどもにエラー落ちして、思わぬハマりポイントになります。E2Eテストでは操作対象の要素が見つからなかった場合もtimeoutのエラーとなり、混乱を避けるため少し長めの設定をしておくことをお勧めします。

なお、Next.jsの記載になりますが、E2Eテストの実行環境のサーバー起動用のスクリプトは、webServerのオプションに記述します。

playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  timeout: 90 * 1000,
  expect: {
    timeout: 30 * 1000,
  },
  testDir: './e2e',
  fullyParallel: false,
  forbidOnly: !!process.env.CI,
  retries: 0,
  workers: 1,
  reporter: 'html',
  use: {
    headless: true,
    ignoreHTTPSErrors: true,
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'pnpm next dev --experimental-https',
    url: 'https://localhost:3000',
    reuseExistingServer: !process.env.CI,
    ignoreHTTPSErrors: true,
    timeout: 120 * 1000,
  },
})

3.Dockerでテスト用データベースの作成

毎回データを初期化し初期データ登録を行いたいため、使い捨てが可能なデータベースをDockerで用意します。

docker-compose-test.yml
services:
  test-db:
    container_name: tests-db
    image: postgres
    restart: always
    ports:
      - '5439:5432'
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: password
      POSTGRES_DB: tests
      TZ: UTC

Dockerの起動と停止は、docker-compose-test.ymlファイルを指定するため、コマンドは下記の記載となります。

package.json
{
  ・・・
  "scripts": {
    ・・・
    "docker:up": "docker compose --file docker-compose-test.yml up -d",
    "docker:down": "docker compose --file docker-compose-test.yml down",
    ・・・
  },
  ・・・
}

4.PrismaのSeedingを利用した初期データの準備

初期データについては、PrismaのSeeding機能を利用しました。
prisma/seed/start.tsファイルを作成し、環境変数を用いてE2Eテスト実行用は、test_seedファイルが実行されるように開発環境と分けるようにしました。

start.ts
import seed from './seed'
import test_seed from './test_seed'

async function main() {
  if (process.env.NODE_ENV === 'test') {
    await test_seed()
  } else {
    await seed()
  }
}

main()
  .catch((e) => console.error(e))
  .finally(async () => {
    await prisma.$disconnect()
  })

なお、start.tsprocess.env.NODE_ENVと環境変数の記述がありますが、GitHub上で環境変数を扱う場合には、GitHub Actionsのシークレットに登録する必要があります。

詳細はこちらをご確認ください。
https://docs.github.com/ja/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository

PlaywrightのE2Eテスト実行前の初期データ登録用のコマンドは下記の記載となります。

package.json
{
  ・・・
  "prisma": {
    "seed": "ts-node  --project tsconfig.local.json prisma/seed/start.ts"
  },
  ・・・
}

5.シナリオテスト用のテストコード作成

「利用者操作 → 管理者操作」の流れでシナリオテストを行いたいため、直列処理で実行されるようテストディレクト配下にe2e/001_user.spec.ts、・・・、e2e/005_admin.spec.tsとファイル名に接頭辞を付け作成しました。
Playwrightは、並列処理を無効にしている場合にファイル名のアルファベット順に実行されます。

今回は、テストコード作成時によく使う関数を2つだけ紹介します。

getByRole関数

基本的なリンク・ボタン操作はgetByRole関数を使いました。正規表現も記述できすごく便利です。
id指定よりもコードの可読性が上がるためお勧めです。
(id属性の指定は、レイアウトや構造変更により変更される可能性があるため、使わないようにしました。)

await page.getByRole('link', { name: /ホーム/i }).click()

waitFor関数

非同期処理でデータ取得するような処理は、描写までの待機時間が必要なため、waitForを使いました。下記コードの例では、waitFor関数の記述がないとエラーになります。

// 非同期で検索データ取得
await page.getByRole('button', { name: /検索/i }).click()
// 要素が見つかるまで待機
await page.locator('table > tbody > tr').first().waitFor()
await page.locator('table > tbody > tr').first().click()

Playwrightは、便利な関数が多々用意されてますので、実装時には公式サイトで調べてみてください。
https://playwright.dev/docs/writing-tests

5.GitHub Actionsで自動化

E2Eテストを自動化するために、GitHub ActionsのWorkflowを利用します。
Workflowの設定は、プロジェクトの.github/workflows/ディレクトリ直下にymlファイルの作成する必要があり、playwright.ymlのファイル名で作成しました。

トリガーは、「devまたはmainブランチへのPull Request作成時」のタイミングとし、ブランチのマージ前に実行結果を確認できるようにします。

これまで準備してきたnpmスクリプトを利用し、下記処理の流れでworkflowの定義を記述しました。
長くなりますので、playwright.ymlファイルに記載の各ジョブの説明は省略させて頂きます。

Workflowの処理の流れ

  1. 各種ライブラリのインストール
  2. Dockerの起動(テスト用データベース)
  3. データベースの初期化
  4. 初期データの登録(Seed)
  5. PlaywrightのE2Eテスト実行
  6. Dockerの停止

作成したworkflowの定義はこちらになります。

playwright.yml
name: Playwright Tests
on:
  pull_request:
    branches: [dev, main]

env:
  DATABASE_URL: ${{secrets.DATABASE_URL}}
  DATABASE_URL: ${{secrets.EMAIL}}
  DATABASE_URL: ${{secrets.PASSWORD}}

jobs:
  playwright-test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm install -g pnpm && pnpm install --no-frozen-lockfile

      - name: Install Playwright Browsers
        run: pnpm exec playwright install --with-deps

      - name: Prisma generate
        run: npx prisma generate

      - name: start docker
        run: pnpm docker:up && pnpm prisma db push --force-reset && pnpm prisma db seed

      - name: Run Playwright tests
        run: pnpm exec playwright test

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: stop docker
        run: pnpm docker:down

実行結果

エラー時

エラー内容がHTML形式でダウンロードできます。

(補足)

ローカルでPlaywrightを実行用に、npmスクリプトを作成しました。
開発環境と環境変数を分けるためdotenvのライブラリのインストールし、各コマンドの先頭には「dotenv -e .env.test」を記述することで、E2Eテスト用環境変数を読み込み実行するようにしました。

package.json
{
    ・・・
    "scripts": {
        "docker:up": "docker compose --file docker-compose-test.yml up -d",
        "docker:down": "docker compose --file docker-compose-test.yml down",
         "e2e:test": "pnpm docker:up && dotenv -e .env.test pnpm prisma db push --force-reset && dotenv -e .env.test npx prisma db seed && dotenv -e .env.test pnpm exec playwright test && pnpm docker:down"
    }
    ・・・
}

おわりに

  • システムにおいて最も重要なユーザーの操作をシナリオテストをとして自動化できたことで、致命的なバグを確実に発見でき、また、改修による既存機能の影響も確認できたため、リリース時の不安が大幅に軽減されました。

  • E2Eテストは時間がかかりメンテナンスが大変とのイメージが強いですが、メインの運用フローのテスト絞り、他の画面は画面表示のみのテストとすることで、コード量も少なく済みメンテナンスにかかるコストも大幅に削減できます。最小限のテストコードでも、各種画面のエラー落ちや検知や致命的なエラーが分かるため、私のプロジェクトではE2Eテストの導入によって大きな恩恵を受けています。

  • E2Eテストの導入や運用が大変に感じている方がいれば、まずは直列で処理するテストを書いてみることをお勧めします。テストコードの設計・構築など導入にあたるハードルはかなり下がります。是非、E2Eテストの作成・自動化を試してみてください。

  • 最後までお読み頂きありがとうございました。

参考文献

Milabo Engineers Blog

Discussion