Playwright テストデータ設計のベストプラクティス:分離・再現・クリーニング戦略

Web アプリケーションのテストにおいて、テストデータの設計は成功を左右する重要な要素です。適切に管理されたテストデータは、テストの信頼性を高め、メンテナンスコストを削減し、チーム全体の開発効率を向上させるでしょう。しかし、実際の開発現場では「テストが他のテストに影響される」「本番データを誤って操作してしまった」「テストが不安定で再現できない」といった課題に直面することも少なくありません。
本記事では、Playwright を使ったテストデータ設計における分離・再現・クリーニングという 3 つの柱に焦点を当て、実践的なベストプラクティスをご紹介します。これらの戦略を理解し実装することで、安定した高品質なテスト環境を構築できるようになるでしょう。
背景
テストデータ管理の重要性
E2E テストにおいて、テストデータは「テストの入力」であり「期待される状態」を定義する基盤となります。適切なテストデータ設計がなければ、以下のような問題が発生しやすくなるのです。
- テスト間でデータが競合し、実行順序によって結果が変わる
- データの初期状態が不明瞭で、テスト失敗の原因特定が困難になる
- 本番環境やステージング環境のデータを誤って変更してしまうリスク
- テストデータのクリーンアップが不十分で、データベースが肥大化する
Playwright におけるテストデータの役割
Playwright は、ブラウザを操作してユーザーの動作を再現する E2E テストフレームワークです。テストコードは UI 操作に集中できますが、その背後にあるデータの管理は開発者の責任となります。
テストデータ設計の全体像を以下の図で示します。
mermaidflowchart TB
TestCode["Playwright<br/>テストコード"] -->|利用| TestData["テストデータ"]
TestData -->|分離戦略| Strategy1["環境分離<br/>データ分離"]
TestData -->|再現戦略| Strategy2["固定データ<br/>Factory パターン"]
TestData -->|クリーニング| Strategy3["自動削除<br/>リセット処理"]
Strategy1 --> Stable["安定した<br/>テスト実行"]
Strategy2 --> Stable
Strategy3 --> Stable
この図が示すように、テストデータは「分離」「再現」「クリーニング」という 3 つの戦略で管理されます。それぞれの戦略が相互に補完し合うことで、安定したテスト環境が実現できるのです。
課題
テストデータ設計における典型的な問題
実際の開発現場では、以下のような課題に直面することがあります。
1. データの競合による不安定なテスト
複数のテストが同じデータを参照・更新すると、実行順序によって結果が変わってしまいます。
typescript// ❌ 悪い例:共有データへの依存
test('ユーザー情報を更新', async ({ page }) => {
// user@example.com を使用(他のテストも同じメールアドレスを使用)
await page.fill('#email', 'user@example.com');
await page.click('#update');
});
このコードでは、user@example.com
という固定のメールアドレスを使用しています。他のテストでも同じメールアドレスが使われていると、テストが並行実行された際にデータが競合し、予期しない結果を招くでしょう。
2. 環境間のデータ不整合
開発環境、ステージング環境、本番環境でデータ構造が異なると、テストの移植性が低下します。
typescript// ❌ 悪い例:環境依存のデータ参照
test('商品を検索', async ({ page }) => {
// 開発環境にしか存在しない ID を直接参照
await page.goto('/products/12345');
});
この実装では、特定の ID(12345)に依存しているため、他の環境では動作しません。環境ごとにテストコードを書き換える必要が生じてしまいます。
3. テスト後のデータ残留
テスト実行後にデータが削除されないと、次回のテスト実行に影響を与えたり、ストレージが圧迫されたりします。
typescript// ❌ 悪い例:クリーンアップなし
test('新規アカウント作成', async ({ page }) => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#signup');
// データが残ったまま終了
});
このテストは新規ユーザーを作成しますが、テスト後にそのユーザーを削除していません。次回の実行時に「ユーザー名が既に存在する」というエラーが発生する可能性があります。
課題の影響範囲
これらの課題は、以下のような状態遷移図で表現できます。
mermaidstateDiagram-v2
[*] --> TestStart: テスト開始
TestStart --> DataConflict: データ競合発生
TestStart --> EnvMismatch: 環境不整合
TestStart --> NoCleanup: クリーンアップなし
DataConflict --> TestFail: テスト失敗
EnvMismatch --> TestFail: テスト失敗
NoCleanup --> DataAccumulate: データ蓄積
DataAccumulate --> Performance: パフォーマンス低下
TestFail --> Debug: デバッグ作業
Debug --> TimeWaste: 時間浪費
Performance --> TimeWaste
TimeWaste --> [*]
この図から、テストデータの問題が最終的に時間の浪費という共通の結果に至ることがわかります。適切な戦略を導入することで、これらの問題を未然に防げるでしょう。
解決策
1. 分離戦略:環境とデータの独立性を確保
環境分離の実装
環境変数を使って、テスト環境を本番環境から完全に分離します。
typescript// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL:
process.env.TEST_BASE_URL || 'http://localhost:3000',
},
});
この設定により、環境変数 TEST_BASE_URL
でテスト対象の URL を切り替えられます。本番環境の URL をハードコードすることなく、安全にテストを実行できるでしょう。
環境ごとの設定ファイルを用意することで、より柔軟な管理が可能です。
typescript// config/test.config.ts
export const testConfig = {
apiBaseUrl:
process.env.API_BASE_URL || 'http://localhost:8080/api',
dbConnection: process.env.TEST_DB_URL,
testDataPrefix: 'test_',
};
この設定ファイルは、API のベース URL やデータベース接続情報、テストデータの接頭辞を一元管理します。接頭辞を付けることで、テストデータと本番データを明確に区別できますね。
データ分離の実装
各テストで独自のデータセットを使用し、他のテストへの影響を排除します。
typescript// fixtures/user.fixture.ts
import { test as base } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
type UserFixture = {
uniqueUser: {
email: string;
username: string;
password: string;
};
};
このコードでは、fixture として一意なユーザー情報を生成する型を定義しています。UUID を使うことで、テストごとに完全にユニークなデータを確保できるのです。
次に、fixture の実装を追加します。
typescriptexport const test = base.extend<UserFixture>({
uniqueUser: async ({}, use) => {
const uniqueId = uuidv4();
const user = {
email: `test-${uniqueId}@example.com`,
username: `testuser-${uniqueId}`,
password: 'TestPass123!',
};
await use(user);
},
});
この fixture は、テストが実行されるたびに新しい UUID を生成し、それをメールアドレスとユーザー名に組み込みます。これにより、並行実行されるテスト間でデータが競合することはありません。
実際のテストでは、以下のように使用します。
typescript// tests/user.spec.ts
import { test } from '../fixtures/user.fixture';
import { expect } from '@playwright/test';
test('新規ユーザー登録', async ({ page, uniqueUser }) => {
await page.goto('/signup');
await page.fill('#email', uniqueUser.email);
await page.fill('#username', uniqueUser.username);
await page.fill('#password', uniqueUser.password);
await page.click('#submit');
await expect(
page.locator('.success-message')
).toBeVisible();
});
このテストは uniqueUser
fixture を使用することで、毎回異なるユーザー情報でテストを実行します。他のテストが同時に実行されていても、データの競合は発生しないでしょう。
2. 再現戦略:一貫性のあるテストデータの生成
Factory パターンの実装
テストデータの生成ロジックを Factory パターンで集約し、一貫性を保ちます。
typescript// factories/user.factory.ts
import { faker } from '@faker-js/faker';
export class UserFactory {
static create(overrides?: Partial<User>): User {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
username: faker.internet.userName(),
password: 'DefaultPass123!',
createdAt: new Date(),
...overrides,
};
}
}
UserFactory
クラスは、Faker ライブラリを使ってランダムなユーザーデータを生成します。overrides
パラメータにより、必要な項目だけをカスタマイズできる柔軟性も備えています。
複数のユーザーを一括生成する機能も追加しましょう。
typescriptexport class UserFactory {
// ... create メソッド
static createMany(
count: number,
overrides?: Partial<User>
): User[] {
return Array.from({ length: count }, () =>
this.create(overrides)
);
}
static createAdmin(overrides?: Partial<User>): User {
return this.create({
role: 'admin',
permissions: ['read', 'write', 'delete'],
...overrides,
});
}
}
createMany
メソッドは指定された数のユーザーを生成し、createAdmin
メソッドは管理者権限を持つユーザーを生成します。このように役割ごとに専用のメソッドを用意することで、テストコードの可読性が向上しますね。
テストでは以下のように使用します。
typescript// tests/admin.spec.ts
import { test, expect } from '@playwright/test';
import { UserFactory } from '../factories/user.factory';
test('管理者ダッシュボードへのアクセス', async ({
page,
}) => {
const admin = UserFactory.createAdmin();
// API 経由で管理者ユーザーを作成
await createUserViaAPI(admin);
await page.goto('/login');
await page.fill('#email', admin.email);
await page.fill('#password', admin.password);
await page.click('#login');
await expect(
page.locator('.admin-dashboard')
).toBeVisible();
});
このテストは Factory パターンを使って管理者ユーザーを生成し、そのユーザーでログインして管理者ダッシュボードにアクセスできることを検証します。テストデータの生成ロジックが集約されているため、メンテナンスが容易です。
固定データセットの管理
特定のシナリオで必要な固定データは、JSON ファイルで管理します。
json// data/products.json
{
"products": [
{
"id": "prod-001",
"name": "テスト商品A",
"price": 1000,
"stock": 10,
"category": "electronics"
},
{
"id": "prod-002",
"name": "テスト商品B",
"price": 2000,
"stock": 5,
"category": "books"
}
]
}
この JSON ファイルは、商品のマスターデータとして使用します。ID や価格などの情報を一箇所で管理できるため、テストの変更に強い設計となります。
固定データを読み込むヘルパー関数を作成します。
typescript// helpers/data-loader.ts
import * as fs from 'fs';
import * as path from 'path';
export class DataLoader {
static load<T>(filename: string): T {
const filePath = path.join(
__dirname,
'../data',
filename
);
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
static async seedDatabase(data: any[]): Promise<void> {
// データベースへのシード処理
for (const item of data) {
await insertIntoDatabase(item);
}
}
}
DataLoader
クラスは、JSON ファイルを読み込んでパースする機能と、データベースにデータを投入する機能を提供します。テストの前処理として、必要なマスターデータを簡単に準備できるでしょう。
実際の使用例は以下の通りです。
typescript// tests/product.spec.ts
import { test, expect } from '@playwright/test';
import { DataLoader } from '../helpers/data-loader';
test.beforeAll(async () => {
const { products } = DataLoader.load('products.json');
await DataLoader.seedDatabase(products);
});
test('商品検索機能', async ({ page }) => {
await page.goto('/products');
await page.fill('#search', 'テスト商品A');
await page.click('#search-button');
await expect(
page.locator('.product-card').first()
).toContainText('テスト商品A');
});
このテストは、beforeAll
フックで商品データをデータベースに投入してから、商品検索のテストを実行します。固定データを使うことで、テスト結果の予測可能性が高まりますね。
3. クリーニング戦略:テスト後のデータ削除
自動クリーンアップの実装
各テストの後に、作成したデータを自動的に削除する仕組みを構築します。
typescript// fixtures/cleanup.fixture.ts
import { test as base } from '@playwright/test';
type CleanupFixture = {
cleanup: {
addUser: (userId: string) => void;
addProduct: (productId: string) => void;
};
};
export const test = base.extend<CleanupFixture>({
cleanup: async ({}, use) => {
const userIds: string[] = [];
const productIds: string[] = [];
const cleanup = {
addUser: (userId: string) => userIds.push(userId),
addProduct: (productId: string) =>
productIds.push(productId),
};
await use(cleanup);
// テスト終了後にクリーンアップ実行
await Promise.all([
deleteUsersViaAPI(userIds),
deleteProductsViaAPI(productIds),
]);
},
});
この fixture は、テスト中に作成されたリソースの ID を記録し、テスト終了後に一括削除します。addUser
や addProduct
メソッドを呼び出すだけで、自動的にクリーンアップ対象として登録されるのです。
実際のテストでは以下のように使用します。
typescript// tests/order.spec.ts
import { test, expect } from '../fixtures/cleanup.fixture';
import { UserFactory } from '../factories/user.factory';
test('注文処理のテスト', async ({ page, cleanup }) => {
const user = UserFactory.create();
const userId = await createUserViaAPI(user);
cleanup.addUser(userId); // クリーンアップ対象として登録
await page.goto('/login');
await page.fill('#email', user.email);
await page.fill('#password', user.password);
await page.click('#login');
// 注文処理のテスト...
await expect(
page.locator('.order-success')
).toBeVisible();
// テスト終了後、ユーザーは自動的に削除される
});
このテストは、ユーザーを作成した直後に cleanup.addUser()
を呼び出しています。テストが成功しても失敗しても、fixture がユーザーの削除を保証するため、データの残留が防げるでしょう。
トランザクションベースのクリーンアップ
データベースを使用する場合は、トランザクションを活用してテスト後にロールバックする方法も有効です。
typescript// helpers/db-transaction.ts
import { PrismaClient } from '@prisma/client';
export class TestTransaction {
private prisma: PrismaClient;
private transactionId: string | null = null;
constructor() {
this.prisma = new PrismaClient();
}
async begin(): Promise<void> {
// テスト用トランザクションを開始
await this.prisma.$executeRaw`BEGIN`;
this.transactionId = Date.now().toString();
}
async rollback(): Promise<void> {
if (this.transactionId) {
await this.prisma.$executeRaw`ROLLBACK`;
this.transactionId = null;
}
}
}
TestTransaction
クラスは、データベーストランザクションを開始し、テスト終了後にロールバックする機能を提供します。この方法を使えば、テスト中のすべてのデータ変更を一括で元に戻せますね。
トランザクションを使った fixture の実装例です。
typescript// fixtures/transaction.fixture.ts
import { test as base } from '@playwright/test';
import { TestTransaction } from '../helpers/db-transaction';
type TransactionFixture = {
dbTransaction: TestTransaction;
};
export const test = base.extend<TransactionFixture>({
dbTransaction: async ({}, use) => {
const transaction = new TestTransaction();
await transaction.begin();
await use(transaction);
await transaction.rollback();
},
});
この fixture を使うと、テストが自動的にトランザクション内で実行され、終了時にロールバックされます。
typescript// tests/database.spec.ts
import {
test,
expect,
} from '../fixtures/transaction.fixture';
test('データベース操作のテスト', async ({
page,
dbTransaction,
}) => {
// このテスト内のすべてのDB操作はトランザクション内で実行される
await page.goto('/admin/users');
await page.click('#create-user');
await page.fill('#username', 'testuser');
await page.click('#save');
await expect(page.locator('.success')).toBeVisible();
// テスト終了後、すべての変更が自動的にロールバックされる
});
このテストでは、ユーザーの作成操作がトランザクション内で実行されます。テスト終了後、データベースは元の状態に戻るため、次のテストに影響を与えません。
テストデータ戦略の全体フロー
これまでに紹介した 3 つの戦略(分離・再現・クリーニング)を組み合わせた全体のフローを図示します。
mermaidsequenceDiagram
participant Test as テストコード
participant Fixture as Fixture/Factory
participant DB as データベース
participant App as テスト対象アプリ
participant Cleanup as クリーンアップ
Test->>Fixture: テストデータ要求
Fixture->>Fixture: 一意なデータ生成<br/>(分離戦略)
Fixture->>DB: データ作成
DB-->>Fixture: データID返却
Fixture->>Cleanup: クリーンアップ対象登録
Fixture-->>Test: テストデータ返却
Test->>App: UI操作実行
App->>DB: データ参照/更新
DB-->>App: 結果返却
App-->>Test: UI応答
Test->>Test: アサーション検証
Test->>Cleanup: テスト終了
Cleanup->>DB: データ削除
DB-->>Cleanup: 削除完了
Cleanup-->>Test: クリーンアップ完了
この図は、テストデータのライフサイクル全体を示しています。Fixture がデータを生成し、クリーンアップ対象として登録し、テスト終了後に削除するという一連の流れが確認できるでしょう。
具体例
実践例:EC サイトの注文フローテスト
実際の EC サイトの注文フローを例に、これまでの戦略を統合したテストを実装します。
テストシナリオの設定
以下のような注文フローをテストします。
- ユーザーがログインする
- 商品を検索してカートに追加する
- 注文を確定する
- 注文完了メッセージを確認する
データファクトリーの準備
まず、必要なデータファクトリーを作成します。
typescript// factories/order.factory.ts
import { faker } from '@faker-js/faker';
import { UserFactory } from './user.factory';
export class OrderFactory {
static create(overrides?: Partial<Order>): Order {
return {
id: faker.string.uuid(),
userId: faker.string.uuid(),
items: [
{
productId: 'prod-001',
quantity: 1,
price: 1000,
},
],
totalAmount: 1000,
status: 'pending',
createdAt: new Date(),
...overrides,
};
}
static createWithUser(): { user: User; order: Order } {
const user = UserFactory.create();
const order = this.create({ userId: user.id });
return { user, order };
}
}
OrderFactory
は注文データを生成し、createWithUser
メソッドでユーザーと注文をセットで作成できます。関連するデータを一括で生成することで、テストコードがシンプルになりますね。
統合テストの実装
分離・再現・クリーニングのすべての戦略を適用した統合テストです。
typescript// tests/order-flow.spec.ts
import { test, expect } from '@playwright/test';
import { UserFactory } from '../factories/user.factory';
import { DataLoader } from '../helpers/data-loader';
import { v4 as uuidv4 } from 'uuid';
// 環境分離:テスト環境の確認
test.beforeAll(async () => {
expect(process.env.TEST_BASE_URL).toBeDefined();
expect(process.env.TEST_BASE_URL).not.toContain(
'production'
);
});
このコードは、テスト実行前に環境設定を確認します。本番環境でテストが実行されないよう、URL に "production" が含まれていないことをチェックしていますね。
次に、テストデータの準備とクリーンアップの設定を行います。
typescripttest("注文フロー完全テスト", async ({ page }) => {
// 1. データ再現:一意なユーザーを作成
const uniqueId = uuidv4();
const user = UserFactory.create({
email: `test-${uniqueId}@example.com`,
username: `testuser-${uniqueId}`,
});
// API経由でユーザーを作成
const createdUser = await fetch(
`${process.env.API_BASE_URL}/users`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
}
).then((res) => res.json());
const userId = createdUser.id;
この部分では、UUID を使って一意なユーザーを生成し、API 経由でデータベースに作成しています。Factory パターンにより、必要な項目だけをカスタマイズできる柔軟性が保たれています。
ログインから商品検索までの操作を実装します。
typescript try {
// 2. ログイン操作
await page.goto("/login");
await page.fill("#email", user.email);
await page.fill("#password", user.password);
await page.click("#login-button");
await expect(page.locator(".user-menu")).toContainText(user.username);
// 3. 商品検索とカート追加
await page.goto("/products");
await page.fill("#search", "テスト商品A");
await page.click("#search-button");
const firstProduct = page.locator(".product-card").first();
await expect(firstProduct).toContainText("テスト商品A");
await firstProduct.locator(".add-to-cart").click();
await expect(page.locator(".cart-count")).toContainText("1");
この部分では、ユーザーがログインし、商品を検索してカートに追加する一連の操作を実行しています。各ステップで適切なアサーションを行うことで、操作が正しく完了したことを確認できますね。
注文確定とクリーンアップの処理を実装します。
typescript // 4. 注文確定
await page.click(".cart-icon");
await page.click("#checkout-button");
await page.fill("#shipping-address", "東京都渋谷区テスト1-2-3");
await page.fill("#phone", "03-1234-5678");
await page.click("#place-order");
// 5. 注文完了確認
await expect(page.locator(".order-success")).toBeVisible();
await expect(page.locator(".order-success")).toContainText("注文が完了しました");
const orderNumber = await page.locator(".order-number").textContent();
expect(orderNumber).toMatch(/^ORD-\d+$/);
} finally {
// 6. クリーンアップ:作成したデータを削除
await fetch(`${process.env.API_BASE_URL}/users/${userId}`, {
method: "DELETE",
});
}
});
このコードは、注文を確定して完了メッセージを確認した後、finally
ブロックで必ずユーザーを削除します。テストが成功しても失敗しても、データが残らないことが保証されるでしょう。
パフォーマンステストでのデータ管理
大量のテストデータを扱う場合の戦略も見ていきます。
typescript// tests/performance.spec.ts
import { test, expect } from "@playwright/test";
import { UserFactory } from "../factories/user.factory";
test.describe("パフォーマンステスト", () => {
const USERS_COUNT = 100;
const createdUserIds: string[] = [];
test.beforeAll(async () => {
// 大量のテストユーザーを一括作成
const users = UserFactory.createMany(USERS_COUNT);
const promises = users.map((user) =>
fetch(`${process.env.API_BASE_URL}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
})
.then((res) => res.json())
.then((created) => createdUserIds.push(created.id))
);
await Promise.all(promises);
});
この前処理では、100 人のユーザーを並行して作成しています。Promise.all
を使うことで、効率的にデータをセットアップできますね。
パフォーマンステストの本体とクリーンアップを実装します。
typescript test("ユーザー一覧の表示速度", async ({ page }) => {
const startTime = Date.now();
await page.goto("/admin/users");
await page.waitForSelector(".user-list-item");
const loadTime = Date.now() - startTime;
// 100人のユーザーが2秒以内に表示されることを確認
expect(loadTime).toBeLessThan(2000);
const userCount = await page.locator(".user-list-item").count();
expect(userCount).toBeGreaterThanOrEqual(USERS_COUNT);
});
test.afterAll(async () => {
// すべてのテストユーザーを削除
const promises = createdUserIds.map((id) =>
fetch(`${process.env.API_BASE_URL}/users/${id}`, {
method: "DELETE",
})
);
await Promise.all(promises);
});
});
このテストは、100 人のユーザーが 2 秒以内に表示されることを検証します。テスト終了後は、作成したすべてのユーザーを並行削除することで、効率的にクリーンアップを実行できるでしょう。
データ分離の比較表
これまでに紹介した分離戦略を、わかりやすく比較してみましょう。
# | 戦略 | メリット | デメリット | 適用場面 |
---|---|---|---|---|
1 | UUID による一意性確保 | データ競合が完全に防げる | 可読性がやや低下する | 並行実行が必要な場合 |
2 | 環境変数による分離 | 本番環境を保護できる | 環境ごとの設定が必要 | すべてのテスト |
3 | トランザクション分離 | 高速で完全なクリーンアップ | DB トランザクションが必須 | DB を直接操作する場合 |
4 | テストデータ接頭辞 | データの識別が容易 | アプリ側の対応が必要 | 共有環境でのテスト |
5 | 専用テストデータベース | 完全な独立性 | インフラ管理の負担増加 | 大規模プロジェクト |
この表から、それぞれの戦略が異なる場面で有効であることがわかります。プロジェクトの規模や要件に応じて、適切な組み合わせを選択することが重要です。
エラーハンドリングとデータ整合性
テストデータの管理において、エラーが発生した場合の対応も重要です。
typescript// helpers/safe-cleanup.ts
export class SafeCleanup {
private resources: Map<string, () => Promise<void>> =
new Map();
register(id: string, cleanup: () => Promise<void>): void {
this.resources.set(id, cleanup);
}
async execute(): Promise<void> {
const errors: Error[] = [];
for (const [id, cleanup] of this.resources) {
try {
await cleanup();
} catch (error) {
console.error(`クリーンアップ失敗: ${id}`, error);
errors.push(error as Error);
}
}
if (errors.length > 0) {
console.warn(
`${errors.length}件のクリーンアップが失敗しました`
);
}
}
}
SafeCleanup
クラスは、複数のリソースのクリーンアップを安全に実行します。一部のクリーンアップが失敗しても、他のリソースの削除を継続するため、データの残留を最小限に抑えられるでしょう。
実際の使用例です。
typescript// tests/with-safe-cleanup.spec.ts
import { test, expect } from '@playwright/test';
import { SafeCleanup } from '../helpers/safe-cleanup';
import { UserFactory } from '../factories/user.factory';
test('安全なクリーンアップ付きテスト', async ({ page }) => {
const cleanup = new SafeCleanup();
try {
const user = UserFactory.create();
const userId = await createUserViaAPI(user);
// クリーンアップ処理を登録
cleanup.register(`user-${userId}`, async () => {
await deleteUserViaAPI(userId);
});
await page.goto('/login');
await page.fill('#email', user.email);
await page.fill('#password', user.password);
await page.click('#login');
await expect(page.locator('.dashboard')).toBeVisible();
} finally {
// すべてのクリーンアップを安全に実行
await cleanup.execute();
}
});
このテストは、SafeCleanup
を使ってリソースのクリーンアップを確実に実行します。エラーが発生してもログに記録されるため、問題の特定が容易になりますね。
まとめ
Playwright を使ったテストデータ設計において、分離・再現・クリーニングという 3 つの戦略を適切に組み合わせることで、安定した高品質なテスト環境を構築できます。
本記事で紹介した主要なポイントを振り返りましょう。
分離戦略のポイント
- 環境変数を使って本番環境とテスト環境を完全に分離する
- UUID やタイムスタンプを使って各テストで一意なデータを生成する
- Fixture を活用してテスト間のデータ競合を防ぐ
再現戦略のポイント
- Factory パターンでテストデータの生成ロジックを集約する
- Faker などのライブラリを活用してリアルなテストデータを生成する
- 固定データが必要な場合は JSON ファイルで管理し、一元化する
クリーンアップ戦略のポイント
- Fixture の
finally
ブロックで確実にリソースを削除する - データベースのトランザクションを活用して高速なロールバックを実現する
- 複数のリソースがある場合は
SafeCleanup
のような仕組みで安全に削除する
これらの戦略を組み合わせることで、以下のような効果が期待できます。
- テストの信頼性向上:データ競合や環境依存の問題が解消され、安定したテスト結果が得られます
- 開発効率の向上:テストの失敗原因が明確になり、デバッグ時間が短縮されます
- メンテナンス性の向上:テストデータの管理が集約され、変更に強い設計になります
- チーム全体の生産性向上:誰でも同じ環境でテストを実行でき、並行開発がスムーズになります
テストデータの適切な管理は、単なるテストの問題ではなく、プロジェクト全体の品質とスピードに直結する重要な要素です。本記事で紹介した戦略を、ぜひ皆さんのプロジェクトに取り入れてみてください。最初は小さな範囲から始めて、徐々に適用範囲を広げていくことをお勧めします。
安定したテスト環境があれば、自信を持ってコードをリファクタリングでき、新機能の追加もスムーズに進められるでしょう。テストデータ設計への投資は、必ず開発体験の向上という形で返ってくるはずです。
関連リンク
- article
Playwright テストデータ設計のベストプラクティス:分離・再現・クリーニング戦略
- article
Playwright コマンド&テストランナー チートシート【保存版スニペット集】
- article
Playwright セットアップ完全手順【2025】:インストールから初テスト作成まで
- article
【2025 年版】Playwright vs Cypress vs Selenium 徹底比較:速度・安定性・学習コストの最適解
- article
【2025 年最新】Playwright 入門:E2E テストの基本・特徴・できること完全ガイド
- article
Playwright × Docker:本番環境に近い E2E テストを構築
- article
GPT-5 失敗しないエージェント設計:プランニング/自己検証/停止規則のアーキテクチャ
- article
Remix でスケーラブルなディレクトリ設計:routes/リソース/ユーティリティ分割
- article
Preact でスケーラブルな状態管理:Signals/Context/外部ストアの責務分離
- article
Emotion チートシート:css/styled/Global/Theme の即戦力スニペット 20
- article
Playwright テストデータ設計のベストプラクティス:分離・再現・クリーニング戦略
- article
NotebookLM の始め方:アカウント準備から最初のノート作成まで完全ガイド
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来