1.はじめに
現在Claude Codeを利用してチャットアプリを作成しており、
Playwrightを使ってE2Eテストを行っております。
こちらもJest同様にClaude Codeが作成したテストコードを
ちゃんと理解する必要があるなと思い、
本記事を作成しながら学習して理解を深めようと思った次第です。
2.PlayWright、E2Eテスト
2.1 PlayWrightとは
Playwrightとは、Microsoftが開発・提供している
Webブラウザの操作を自動化するためのオープンソースのライブラリ・フレームワークです。
特徴
-
クロスブラウザ自動化
単一のAPIで複数のブラウザ(Chromium, Firefox, WebKit)のテストを自動化でき、
ブラウザ間の互換性テストを効率的に実施できます。 -
包括的なテストツール
クリーンショットや動画のキャプチャ、ネットワークトラフィックのインターセプト(傍受)、
デバイスのエミュレーションなど、様々なテストシナリオに対応する機能を提供。 -
信頼性と安定性
ページ遷移や読み込み中に適切に待機する機能や自動的なリトライ機能があり、
安定したテストの実行が可能です。 -
デバッグ機能
テスト中のスクリーンショットやネットワークトレースを取得できる機能、
実行履歴を振り返れる「トレースビュー」など、
問題のデバッグに役立つツールが充実しています。 -
多言語対応
JavaScript、TypeScript、Python、C#、Javaなど、
さまざまなプログラミング言語でテストコードを記述できます。 -
CI/CDへの組み込みやすさコマンドラインからの実行が可能で、
GitHub ActionsなどのCI/CDツールが組み込みやすい構造になっています。
2.2 E2Eテストとは
システム全体の動作を、実際のエンドユーザーの視点から、
最初から最後まで(エンドからエンドまで)検証するテスト手法。
システム構成要素やデータベース、API、ネットワークなど、
多岐にわたるサブシステムが連携して機能することを確認し、
実際の利用シーンを再現することで、アプリケーションの品質を高め、
ユーザーが期待通りの体験を得られることを保証します。
特徴
-
ユーザー視点での検証
実際のユーザーがシステムをどのように利用するかを想定したシナリオに沿って、
システム全体の動作を確認します。 -
システム全体の網羅性
単体のモジュールや特定の機能の組み合わせだけでなく、システム全体が統合され、
ユーザーが期待する一連のプロセスを正しく実行できるかを確認します。 -
本番環境に近い環境での実施
システムがユーザーに提供する実際の利用体験に近づけるため、
ネットワーク接続、データベース、他のアプリケーション連携など、
システムを構成する要素を含めた、できる限り本番環境に近い条件でテストを行います。
メリット
-
品質の向上とバグの早期発見
システム全体にわたる包括的なテストにより、
個別のテストでは発見できない潜在的な問題や、
サブシステム間の連携不備に起因するバグを特定できます。 -
ユーザーエクスペリエンスの確保
実際にシステムを操作するユーザー視点でテストを行うことで、
ユーザーが体験する使いやすさや安定性を確認し、顧客満足度を高めることができます。 -
信頼性の高いリリース
ユーザーがシステムを使い始める前に、
エンドからエンドまでのあらゆる操作が問題なく機能することを検証できるため、
リリース後のトラブルや不満を回避できます。
E2Eテストの自動化
-
効率と品質の向上
E2Eテストは、手動で行うと非常に多くの時間がかかるため、
UI操作の自動化ツールなどを用いて自動化することが一般的です。 -
定期的なテストの実施
アプリケーションの更新や変更が頻繁に行われる現代において、
E2Eテストを定期的に自動で実行することで、
アプリケーションを常に最新かつ安全な状態に保ち、品質を維持することができます。
2.PlayWright環境構築(Next.js)
2.1 プロジェクト作成、インストール
# Next.jsプロジェクト作成
npx create-next-app@latest e2e-sample --yes
cd e2e-sample
# Playwrightインストール
npm init playwright@latest
# フォルダ名やGitHub Actions workflowの作成、
# Playwright browsersのインストール有無の確認があります、
# 本プロジェクトはフォルダはtests、他は全てYesにしてます。
2.2 設定ファイル
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests', // テストコードを置くディレクトリ(./tests)を指定。
/* Run tests in files in parallel */
fullyParallel: true, // ファイル単位で並列実行を許可。テストが速くなる。
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, // テストコードにtest.only(...)が残っていたら、CI で強制エラーにする。(特定のテストだけ実行してしまう事故を防ぐ)
/* Retry on CI only */
retries: process.env.CI ? 2 : 0, // CI環境では失敗したテストを 最大2回リトライ。(ローカルはリトライ無し)
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, // CIでは並列数を1に固定(リソース制限を避ける)。
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', // テスト結果を HTML レポートで出力。
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000', // page.goto('/') のように書いた場合、http://localhost:3000/ に展開される。
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', // テストが失敗してリトライされる場合、最初のリトライ時にトレースを収集。
},
/* Configure projects for major browsers */
// テストを実行するブラウザ環境を定義。
// devices は Playwright が持つデバイスプリセット(画面サイズや UA の設定)
// デフォルトでは Chrome / Firefox / Safari 相当で実行
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// テスト実行前に npm run dev を実行して ローカル開発サーバーを起動
// 'http://localhost:3000' にアクセスできる状態になるまで待機
// サーバー起動を最大 120 秒まで待つ
// 既にサーバーが起動しているなら使い回す
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: process.env.PW_REUSE_SERVER === '1',
timeout: 120000,
},
});
package.json
scriptsにe2eテスト用コマンドを追加
"scripts": {
"test:e2e": "playwright test"
}
.github/workflow/playwright.yml
Github Actionsの確認でYesにすると作成されます。
本記事では、こちらのファイルの実行は割愛します。
name: Playwright Tests #ワークフローの名前。GitHub Actions の実行画面で表示
# トリガー(実行条件) を設定。
on:
push:
branches: [ main, master ] # main, masterブランチPush後にテスト実行
pull_request:
branches: [ main, master ] # main, masterブランチへのPRトリガーでテスト実行
jobs: # 実行する処理のまとまり
test: # ジョブ名
timeout-minutes: 60 # テスト実行が 60 分を超えたら強制終了
runs-on: ubuntu-latest # 実行環境は Ubuntu の最新バージョン。
steps: # ジョブ内で実行する手順。
- uses: actions/checkout@v4 # リポジトリのコードをGitHub Actionsの環境にチェックアウト
- uses: actions/setup-node@v4 # Node.js 環境をセットアップ。
with:
node-version: lts/* # lts/* → Node.js の LTS(安定版)バージョンを使う。
- name: Install dependencies
run: npm ci # npm ci を使うことで package-lock.json を厳密に反映
- name: Install Playwright Browsers
run: npx playwright install --with-deps # Playwright が利用するブラウザ(Chromium, Firefox, WebKit など)と必要な依存関係をインストール。
- name: Run Playwright tests
run: npx playwright test #Playwright のテストを実行
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # ジョブがキャンセルされなかった場合のみアップロード
with:
name: playwright-report
path: playwright-report/ # playwright-report/ ディレクトリを成果物としてアップロード
retention-days: 30 # 30日間保持。
# 実行後、GitHub Actions の画面から Playwright のレポートをダウンロードできる
3.本実装とテストコード実装
3.1 src/app/signup/page.tsx(サインアップ画面)
"use client"
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function SignupPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<h1 className="text-2xl font-bold mb-4">アカウント作成</h1>
<input
type="email"
placeholder="メールアドレス"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 mb-2 w-64"
/>
<input
type="password"
value={password}
placeholder="パスワード"
onChange={(e) => setPassword(e.target.value)}
className="border p-2 mb-2 w-64"
/>
<button
data-testid="signup-button"
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={() => {
// 簡易的にLocalStorageに保存
localStorage.setItem("user", JSON.stringify({ email }));
router.push("/signin");
}}
>
登録
</button>
</div>
)
}
3.2 src/app/signin/page.tsx(サインイン画面)
"use client"
import { useRouter } from "next/navigation"
import { useState } from "react";
export default function SigninPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<h1 className="text-2xl font-bold mb-4">ログイン</h1>
<input
type="email"
placeholder="メールアドレス"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 mb-2 w-64"
/>
<input
type="password"
value={password}
placeholder="パスワード"
onChange={(e) => setPassword(e.target.value)}
className="border p-2 mb-2 w-64"
/>
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={() => {
const storedUser = localStorage.getItem("user");
if (!storedUser) {
alert("メールアドレスを入力してください");
return;
}
const accountEmail: { email: string } | null = JSON.parse(storedUser);
if (accountEmail?.email === email) {
router.push("/dashboard");
} else {
alert("メールアドレスに誤りがあります");
}
}}
>
ログイン
</button>
</div>
)
}
3.3 src/app/dashboard/page.tsx(ダッシュボード画面)
"use client"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react";
export default function DashboardPage() {
const router = useRouter();
const [user, setUser] = useState<{ email: string } | null>(null);
useEffect(() => {
const storedUser = localStorage.getItem("user");
if (storedUser) {
setUser(JSON.parse(storedUser));
} else {
router.push("/signin");
}
}, [router]);
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<h1 className="text-2xl font-bold mb-4">ダッシュボード</h1>
{user && <p className="mb-4">ようこそ {user.email} さん</p>}
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={() => {
localStorage.removeItem("user");
router.push("/signin");
}}
>
ログアウト
</button>
</div>
)
}
3.4 src/app/tests/auth.spec.ts
(サインアップ→サインイン→サインアウトまで一連の流れをテスト)
import test, { expect } from "@playwright/test";
test.describe('認証後', () => {
})
test("サインアップ→サインイン→ログアウト", async ({ page }) => {
// サインアップページへ
await page.goto("http://localhost:3000/signup");
// サインアップ
await page.fill('input[placeholder="メールアドレス"]', "test@example.com");
await page.fill('input[placeholder="パスワード"]', "password123");
await page.click('[data-testid="signup-button"]');
// サインインページへ遷移したが確認
await expect(page).toHaveURL("http://localhost:3000/signin");
// サインイン
await page.fill('input[placeholder="メールアドレス"]', 'test@example.com');
await page.fill('input[placeholder="パスワード"]', "password123");
await page.click("text=ログイン");
// ダッシュボードへ遷移
await page.goto("http://localhost:3000/dashboard");
await expect(page.locator("text=ようこそ test@example.com さん")).toBeVisible();
// ログアウト
await page.click("text=ログアウト");
// サインイン画面へ戻る
await expect(page).toHaveURL("http://localhost:3000/signin");
});
4.e2eテスト実行
npm run test:e2e
> e2e-sample@0.1.0 test:e2e
> playwright test
Running 9 tests using 4 workers
9 passed (11.5s)
To open last HTML report run:
npx playwright show-report
5.Playwrightでよく使う関数
| コマンド / メソッド | 役割・用途 | 使用例 |
|---|---|---|
page.goto(url) |
指定した URL に移動する | await page.goto("http://localhost:3000") |
page.click(selector) |
ボタンやリンクをクリック | await page.click("text=ログイン") |
page.fill(selector, value) |
入力フィールドに文字を入力 | await page.fill('input[name="email"]', "test@example.com") |
page.type(selector, text) |
タイプ入力(1文字ずつ入力する動作を再現) | await page.type("#message", "Hello") |
page.locator(selector) |
要素を取得(複数要素の操作や待機に便利) | const btn = page.locator("button"); await btn.click(); |
page.getByRole(role, options) |
アクセシビリティロールで要素を取得(推奨) | await page.getByRole("button", { name: "ログイン" }).click(); |
page.getByTestId(testId) |
data-testid 属性で要素を取得 |
await page.getByTestId("signup-button").click(); |
expect(locator).toBeVisible() |
要素が表示されていることを検証 | await expect(page.locator("text=ようこそ")).toBeVisible(); |
expect(locator).toHaveText(text) |
要素のテキストが一致するか検証 | await expect(page.locator("h1")).toHaveText("Dashboard"); |
expect(page).toHaveURL(url) |
現在の URL が一致するか検証 | await expect(page).toHaveURL("/dashboard"); |
page.waitForSelector(selector) |
要素が DOM に現れるまで待機 | await page.waitForSelector("#loading-done") |
page.waitForResponse(urlOrPredicate) |
特定のリクエストのレスポンスを待つ | await page.waitForResponse(res => res.url().includes("/api/login")) |
page.waitForTimeout(ms) |
強制的に待機(デバッグ用途のみ推奨) | await page.waitForTimeout(1000); |
page.screenshot(options) |
スクリーンショットを保存 | await page.screenshot({ path: "test.png", fullPage: true }); |
page.context().storageState(options) |
localStorage / Cookie 状態を保存・復元 | await page.context().storageState({ path: "storageState.json" }); |
test.describe(name, fn) |
テストのグルーピング | test.describe("認証系", () => { ... }); |
test.beforeEach(fn) / test.afterEach(fn)
|
各テストの前後処理(共通の初期化やクリーンアップ) | test.beforeEach(async ({ page }) => { await page.goto("/signin"); }); |
test.skip() / test.only()
|
特定テストのスキップ / そのテストだけ実行 | test.only("ログインのみテスト", async ({ page }) => { ... }); |
6.Playwrightのテスト実行コマンドオプション
| フラグ / コマンド | 役割 | 使い方・例(ターミナル) |
|---|---|---|
npx playwright test |
テスト実行(デフォルト) | npx playwright test |
npx playwright test <ファイル> |
指定ファイルだけ実行 | npx playwright test tests/auth.spec.ts |
npx playwright test <file>:<line> |
指定ファイルの特定行の test を実行(1つのテストだけ) | npx playwright test tests/auth.spec.ts:12 |
--project=<name> |
playwright.config の project(例: chromium / webkit)だけ実行 |
npx playwright test --project=webkit |
--headed |
ヘッドレスではなくブラウザ表示で実行(デバッグに便利) | npx playwright test --headed |
--debug |
Inspector を開いて対話的デバッグ(自動で --headed --workers=1 --timeout=0 等が効く) |
npx playwright test --debug |
--ui |
UI モード(テスト一覧 UI を開いて選択実行など) | npx playwright test --ui |
--trace=<mode> |
トレース記録(on / off / on-first-retry / retain-on-failure 等) |
npx playwright test --trace=on-first-retry |
--retries=<n> |
失敗時のリトライ回数 | npx playwright test --retries=2 |
--workers <n> / -j <n>
|
並列ワーカー数(1 なら逐次実行) |
npx playwright test --workers=2 |
--grep "<regex>" |
テスト名にマッチするテストだけ実行 | npx playwright test --grep "ログイン" |
--grep-invert "<regex>" |
マッチしないテストだけ実行 | npx playwright test --grep-invert "slow" |
--repeat-each <n> |
各テストを N 回繰り返し実行(安定化確認) | npx playwright test --repeat-each=5 |
--last-failed |
前回失敗したテストだけ再実行 | npx playwright test --last-failed |
--only-changed |
Git 差分のテストのみ実行(開発中に便利) | npx playwright test --only-changed |
--update-snapshots / -u
|
スナップショットを更新(visual/assertion 用) | npx playwright test -u |
--reporter=<name> |
レポーター指定(例: list / line / html) |
npx playwright test --reporter=html |
--output <dir> |
アーティファクト出力先(デフォルト test-results) |
npx playwright test --output=artifacts |
--timeout <ms> |
各テストのタイムアウト(ms、0 は無制限) | npx playwright test --timeout=60000 |
--global-timeout <ms> |
全体の最大実行時間(ms) | npx playwright test --global-timeout=600000 |
--max-failures <N> / -x
|
失敗が N 件に到達したら途中終了(速くフィードバックを得たいとき) |
npx playwright test -x(=stop after first failure) |
--forbid-only |
CI 用。test.only があれば失敗として終了 |
npx playwright test --forbid-only |
--list |
テスト収集のみ(実行せず一覧表示) | npx playwright test --list |
--pass-with-no-tests |
テストが0件でも成功扱いにする(CI 用) | npx playwright test --pass-with-no-tests |
7.さいごに
PlayWrightはJestと比べても色々な機能があって、便利な分、
学習コストや設定方法についてもまだまだ理解が浅いので、
知識を深めながら、手を動かして記憶していく作業をしないと、
全然使いこなせない感じがします。
サンプルソースでテストコード書いたけど、
ログイン→ダッシュボード画面遷移が上手く動かず、イライラしました。