テスト戦略 レッスン3
結合テストとE2E
複数コンポーネントを組み合わせて検証する結合テストと、ユーザー視点で確認するE2Eテストの役割を学ぶ
単体テストだけでは見えないもの
個々の関数が正しくても、それらを組み合わせたときに問題が起きることがあります。 たとえば「関数Aが返すデータ形式を関数Bが正しく期待していない」といった境界のズレは、単体テストだけでは検出できません。
// ① repository.ts: DBアクセス層
async function findUserById(db, id) {
const row = await db.query("SELECT * FROM users WHERE id = ?", [id]);
return row; // 実は配列を返している(想定は単一オブジェクト)
}
// ② service.ts: ビジネスロジック層
async function getUserName(db, id) {
const user = await findUserById(db, id);
return user.name; // user が配列だと undefined になり気づきにくい
}このような「単体では正しいが組み合わせると壊れる」パターンを検出するのが結合テストの役割です。
結合テストの書き方
結合テストでは、実際のデータベースや外部依存に近いもの(テスト用DB、インメモリDB等)を使い、 複数のモジュールをまたいだ振る舞いを検証します。
import { describe, it, expect, beforeEach } from "vitest";
import { setupTestDatabase } from "./test-utils";
import { getUserName } from "./service";
describe("getUserName (結合テスト)", () => {
let db;
beforeEach(async () => {
db = await setupTestDatabase(); // テスト用DBを毎回初期化
await db.seed("users", [{ id: 1, name: "田中太郎" }]);
});
it("実際のDBから正しくユーザー名を取得できる", async () => {
const name = await getUserName(db, 1);
expect(name).toBe("田中太郎");
});
});E2Eテストの役割
E2E(End-to-End)テストは、実際のブラウザを使い、ユーザーが行う操作を最初から最後まで再現します。 「ログインしてカートに商品を追加し、決済を完了する」といった主要な業務フローの検証に向いています。
import { test, expect } from "@playwright/test";
test("ユーザーが商品を購入できる", async ({ page }) => {
await page.goto("/login");
await page.fill("input[name=email]", "user@example.com");
await page.fill("input[name=password]", "password123");
await page.click("button[type=submit]");
await page.click("text=商品Aをカートに追加");
await page.click("text=購入手続きへ");
await expect(page.locator("text=注文が完了しました")).toBeVisible();
});ポイント
E2Eはすべての画面・パターンを網羅する必要はありません。売上に直結する主要なフローに絞って書くのが実務的です。
結合テスト・E2Eテストの注意点
- • テスト用DBは各テストの前後で確実にリセットし、テスト間の依存を作らない
- • E2Eの待機処理は固定の
sleepではなく、要素の出現を待つ形にしてフレーキーさを減らす - • 外部の決済APIなど本番に影響するものはテスト用のサンドボックス環境を使う
ポイント
- • 結合テストは単体テストでは見えない「境界のズレ」を検出する
- • E2Eテストはユーザー視点でシステム全体の一気通貫の動作を確認する
- • E2Eは主要な業務フローに絞り、網羅性より安定性を優先する
- • テスト間の依存を作らないよう、状態を都度リセットする
確認クイズ
1 / 3結合テストが検出しやすい問題として適切なものはどれか?