Next.js E2E テスト:Playwright 自動化テスト実践ガイド

午前3時。バグチケットに貼られた「緊急」の赤いラベルを見つめながら、私は本番環境で同じページを3回リロードしていました。決済フローがまた壊れたのです。テスト環境では完璧に動いていたのに、なぜ本番だけで動かないのか?
先週のリリースを思い出します。30以上のページを手動でクリックし、十数個のフォームを入力し、3種類のブラウザを行き来しました。それだけやったのに、モバイル端末で一番下までスクロールしないと表示されないボタンの不具合を見逃していました。プロダクトマネージャーからの Slack 通知がポップアップします。「ユーザーからクーポンが使えないって言われてるんだけど」。胃がキリキリと痛み始めました。
その夜、私は決心しました。E2E テストを導入しなければならない。これ以上、人海戦術の手動テストを続けていたら、チームも自分も倒れてしまう。
善は急げ。選定にあたって Cypress、Selenium、Puppeteer、Playwright 一通り検討しました。最終的に Playwright を選んだ理由は、マルチブラウザ対応の強力さと、Cypress よりも設定がシンプルだったからです。導入して最初の一週間で、Playwright は手動テストでは見つけられなかった5つのバグを見つけてくれました。Firefox だけで崩れるレイアウトや、非同期 API の競合状態などです。
この記事では、私が半年間 Next.js プロジェクトで Playwright を運用して得た知見を共有します。正直に言うと、最初は設定ファイルを何度も書き直したり、テストケースを全書き換えしたりと、かなりの試行錯誤がありました。でも今ではフローは完全にスムーズになり、CI/CD で自動化され、コミットのたびにテストが走り、本番のバグは劇的に減りました。
なぜ Playwright を選んだのか(vs Cypress)
Cypress を使ったことがある人なら、その設定の手軽さやドキュメントの親切さはご存知でしょう。では、なぜ私は最終的に Playwright を選んだのでしょうか?
主な理由は3つあります:
1. クロスブラウザサポートの弱さ:Cypress の Firefox や Safari (WebKit) 対応は、長らく実験的だったり不安定だったりしました。基本は Chromium ベースです。これには痛い目に遭わされました。Chrome で完璧に動く決済ページが、Safari で真っ白になったのです。原因は Safari 未対応の CSS プロパティでした。Playwright は Chromium、Firefox、WebKit の3大エンジンをネイティブサポートしており、1セットのテストコードで主要ブラウザをカバーできます。
2. テスト速度:Playwright の並列実行能力は圧倒的です。Cypress は基本直列実行で、50ケース走らせるのに十数分かかっていました。Playwright で 8 worker 並列にすると、同じケースが5分で終わります。CI/CD の実行時間はコストに直結するので、この差は大きいです。
3. API デザイン:正直、Cypress のチェーンメソッド(jQueryライク)から Playwright に移行した当初は戸惑いました。しかし、Playwright の async/await スタイルに慣れると、現代的な JavaScript の書き方に近く、Next.js の Server Components のコードスタイルとも一貫性があることに気づきました。
とはいえ、Cypress が悪いわけではありません。Chrome だけ動けばよく、チームがテストに不慣れなら、Cypress の方が学習コストは低いでしょう。タイムトラベル機能(Time Travel)によるデバッグは本当に素晴らしいです。
しかし、私の要件には Playwright が合っていました:
- クロスブラウザテストが必須
- Next.js/React の経験があり、
async/awaitに抵抗がない - CI 環境での高速なフィードバックが必要
- API ルートや SSR ページのテストもしたい
結論:小規模プロジェクトや初心者チームなら Cypress で素早く始めるのが吉。ある程度の規模があり、長期的に自動化テストに投資するなら Playwright がベストチョイスです。
Next.js + Playwright 設定実践
Playwright のインストールは超簡単です。コマンド3つで終わります:
npm init playwright@latest
# または pnpm
pnpm create playwrightインストール中にいくつか質問されます。おすすめの回答はこちら:
- TypeScript? Yes(型定義の恩恵は絶大です)
- テストディレクトリ? tests(デフォルトでOK)
- GitHub Actions? Yes(CI/CD で使います)
インストールが完了すると、以下のファイルが生成されます:
your-nextjs-project/
├── tests/ # テストケース格納ディレクトリ
│ └── example.spec.ts
├── playwright.config.ts # Playwright 設定ファイル
└── .github/
└── workflows/
└── playwright.yml # CI 設定ファイル設定ファイルの落とし穴と最適解
初期生成される playwright.config.ts は一般的な Web プロジェクト用です。Next.js プロジェクト用に最適化した、私の「半年間の集大成」設定がこちらです:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// テストディレクトリ
testDir: './tests',
// グローバルタイムアウト:1テストあたり30秒
timeout: 30 * 1000,
// アサーションタイムアウト:要素が見つかるまで5秒待つ
expect: {
timeout: 5000,
},
// 失敗時のリトライ回数(CI 環境ではオンにする)
retries: process.env.CI ? 2 : 0,
// 並列 worker 数(私のマシンは8コアなので4に設定)
workers: process.env.CI ? 2 : 4,
// レポート形式
reporter: [
['html'], // HTML レポート生成
['list'], // ターミナルリスト出力
process.env.CI ? ['github'] : ['list'], // CI では GitHub Annotation 形式
],
// Next.js 開発サーバーの起動設定
webServer: {
command: 'npm run dev',
port: 3000,
timeout: 120 * 1000, // 初回ビルドは時間がかかるので長めに
reuseExistingServer: !process.env.CI, // ローカルでは既存サーバーを再利用
},
// テストプロジェクト(ブラウザ別設定)
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// モバイルテスト(必要に応じて)
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
// グローバル設定
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // 失敗時のみトレースを記録(デバッグ神機能)
screenshot: 'only-on-failure', // 失敗時のみスクショ
video: 'retain-on-failure', // 失敗時のみ録画
},
});重要なポイント
webServer.timeoutは長めに:最初は30秒にしていましたが、Next.js の初回ビルド(特に大規模アプリ)は遅く、タイムアウトで落ちまくりました。120秒にしておけば安心です。reuseExistingServer: trueを忘れずに:これがないと、テスト実行のたびに Next.js サーバーが再起動してしまい、開発テンポが悪くなります。ローカルではtrue推奨です。workersは欲張らない:CPU コア数全開にすると、PC がフリーズすることがあります。コア数の半分くらいが適正です。モバイルテストは任意:レスポンシブ対応が重要なアプリなら
Mobile Chromeを入れるべきですが、テスト時間は倍増します。プロジェクトの要件と相談してください。
設定ができたら、サンプルを実行してみましょう:
npx playwright test緑色の passed が出れば環境構築完了です。
画面操作テストのベストプラクティス(Page Object Model)
初心者がやりがちなミスは、テストコード内に直接セレクタを書きまくることです。ログインページのテストで 100 行以上のスパゲッティコードを書き、input[name="email"] があちこちに散乱している…なんてこと、ありませんか? デザイナーが ID を変えた瞬間、全テストが壊れて地獄を見ます。
これを防ぐのが Page Object Model (POM) です。ページ操作をクラスにカプセル化するパターンです。
POM を使わない悪い例
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test('ユーザーログイン', async ({ page }) => {
await page.goto('/login');
// 要素を直接操作(重複多数、変更に弱い)
await page.locator('input[name="email"]').fill('[email protected]');
await page.locator('input[name="password"]').fill('password123');
await page.locator('button[type="submit"]').click();
await expect(page.locator('h1')).toContainText('Dashboard');
});POM でリファクタリング(推奨)
まず Page Object クラスを作ります:
// tests/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly dashboardTitle: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error');
this.dashboardTitle = page.locator('h1');
}
// 操作をメソッド化
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async goto() {
await this.page.goto('/login');
}
// アサーションもメソッド化
async expectLoginSuccess() {
// 画面遷移を待つ
await this.dashboardTitle.waitFor();
await expect(this.dashboardTitle).toContainText('Dashboard');
}
async expectLoginError() {
await expect(this.errorMessage).toBeVisible();
}
}テストコードはこう変わります:
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('ユーザーログイン', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await loginPage.expectLoginSuccess();
});どうですか? “コードがドキュメントのように読める” ようになりました。セレクタが変わっても LoginPage.ts を一行直すだけで全テストが修正されます。
ディレクトリ構成例:
tests/
├── pages/ # Page Objects
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── CheckoutPage.ts
├── fixtures/ # テストデータ
│ └── testData.ts
├── auth.spec.ts # 認証テスト
└── checkout.spec.ts # 購入テストAPI ルートの E2E テスト
Next.js の強みは API Routes ですが、これももちろんテスト対象です。Postman でポチポチ手動テストするのは卒業しましょう。Playwright はブラウザ操作だけでなく、強力な HTTP リクエスト機能も持っています。
基本的な API テスト
// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';
test.describe('ユーザー API テスト', () => {
test('GET /api/users - リスト取得', async ({ request }) => {
const response = await request.get('/api/users');
// ステータスコード検証
expect(response.status()).toBe(200);
// ボディ検証
const users = await response.json();
expect(Array.isArray(users)).toBeTruthy();
expect(users.length).toBeGreaterThan(0);
expect(users[0]).toHaveProperty('email');
});
test('POST /api/users - 重複登録エラー', async ({ request }) => {
const duplicateUser = {
email: '[email protected]',
password: 'password123'
};
const response = await request.post('/api/users', {
data: duplicateUser
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.message).toContain('既に使用されているメールアドレスです');
});
});画面操作 + API 検証のハイブリッドテスト
これが最強のパターンです。「画面から記事を投稿し、API でデータベースに正しく保存されたか確認する」といったシナリオです。
// tests/posts.spec.ts
test('記事投稿の完全フロー', async ({ page, request }) => {
// 1. 画面操作:ログインして記事投稿
await page.goto('/login');
// ... ログイン処理 ...
await page.goto('/posts/new');
await page.fill('input[name="title"]', 'テスト記事');
await page.click('button:has-text("公開")');
// 2. 完了画面への遷移待機
await page.waitForURL(/\/posts\/\d+/);
// 3. API 検証:データが本当に作られたか?
const url = page.url();
const postId = url.split('/').pop();
const response = await request.get(`/api/posts/${postId}`);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.title).toBe('テスト記事');
expect(post.status).toBe('published'); // ステータスもチェック
});この方法で、「画面上は『成功』と出ているのに、DB上は下書き状態のまま」という恐ろしいバグを見つけたことがあります。フロントエンドとバックエンドの整合性をチェックできるのは E2E ならではです。
CI/CD (GitHub Actions) 統合
テストは自動で走ってこそ意味があります。npm init playwright で生成されるワークフローは基本的すぎるので、本番運用に耐えるように強化した設定を紹介します。
本番級 CI 設定 (.github/workflows/playwright.yml)
name: E2E Tests
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
# テスト用データベース (PostgreSQL)
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
NODE_ENV: test
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
# ブラウザバイナリのキャッシュ(高速化の鍵)
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run database migrations
run: npm run db:migrate
- name: Run Playwright tests
run: npx playwright test
# テスト結果(HTMLレポート、スクショ、Trace)を保存
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30設定の改善点解説
- Service Container (Postgres):API テストが落ちないよう、CI 内で本物の DB を立てます。MySQL なら
mysql:8に変えてください。 - ブラウザキャッシュ:Playwright のブラウザダウンロードは時間がかかります。
actions/cacheでキャッシュすることで、2回目以降の実行を高速化します。 - DB Migration:テスト実行前に
npm run db:migrate(Prisma など) でテーブルを作ります。 - Artifact Upload:テスト失敗時、スクリーンショットや Trace ファイルを GitHub Artifacts として保存し、後でダウンロードして調査できるようにします。
デバッグの神機能:Trace Viewer
CI でテストが落ちたとき、ログだけ見ても「何が起きたか」さっぱりわかりません。そこで Playwright の Trace Viewer の出番です。
私の playwright.config.ts 設定では trace: 'on-first-retry' になっています。これは「一度失敗してリトライした時、詳細なトレースを取る」という設定です。生成された trace.zip を開くと:
- テスト実行のタイムライン
- 各ステップでの画面スナップショット(DOMも見れる)
- ネットワークリクエスト詳細
- コンソールログ
これらがプレイヤーのように再生できます。「ああ、このボタンをクリックした瞬間、API が 500 エラーを返したから次に行けなかったのか」と一発で分かります。これを知ってからデバッグ時間が10分の1になりました。
まとめ
Playwright による E2E テストの導入は、最初は少し学習コストがかかりますが、そのリターンは計り知れません。「リリース前の胃の痛み」から解放される最良の投資です。
まずは主要なハッピーパス(正常系)、例えば「ログインして、商品を選んで、購入する」といったコア機能からテストを書き始めてください。カバレッジ100%を目指すのは後でいいです。動くテストが一つあるだけで、安心感は段違いですから。
FAQ
Cypress と Playwright、今から始めるならどっち?
E2E テストは不安定(Flaky)になりがちですが?
すべてのページに Page Object を作るべきですか?
4 min read · 公開日: 2026年1月7日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます