テストの種類
テストの種類
ソフトウェアテストにはさまざまな種類があります。このチャプターでは、単体テスト、結合テスト、E2Eテストの違いと、それぞれの目的について学びます。
テストレベルの分類
テストは、テストする範囲によって3つのレベルに分類されます。
テストピラミッド
/\
/ \ E2Eテスト(End-to-End)
/____\ - 少数
/ \ - 遅い
/ \ - 高コスト
/ 結合 \
/____________\ 結合テスト(Integration)
/ \ - 中程度の数
/ \- 中程度の速度
/ 単体 \
/__________________\ 単体テスト(Unit)
- 多数
- 高速
- 低コスト
[!INFO] テストピラミッドの原則
テストピラミッドは、理想的なテストの配分を表しています。基盤となる単体テストを多く、上位の統合テストやE2Eテストは適度な数にすることで、効率的かつ効果的なテスト戦略を構築できます。
単体テスト(Unit Test)
概要
単体テストは、個々の関数やクラスなどの小さな単位をテストします。
| 項目 | 詳細 |
|---|---|
| テスト範囲 | 1つの関数、1つのクラス |
| 実行速度 | 非常に高速(ミリ秒単位) |
| テスト数 | 最も多い(数百〜数千) |
| 依存関係 | 外部依存を排除(モック使用) |
単体テストの例
// テスト対象の関数
function calculateTotalPrice(price: number, quantity: number, taxRate: number): number {
return price * quantity * (1 + taxRate);
}
// 単体テスト
import { test, expect } from '@playwright/test';
test.describe('calculateTotalPrice', () => {
test('価格、数量、税率から合計金額を計算する', () => {
const result = calculateTotalPrice(1000, 2, 0.1);
expect(result).toBe(2200); // 1000 * 2 * 1.1 = 2200
});
test('数量が0の場合、合計金額は0になる', () => {
const result = calculateTotalPrice(1000, 0, 0.1);
expect(result).toBe(0);
});
test('税率が0の場合、税込み価格は元の価格と同じ', () => {
const result = calculateTotalPrice(1000, 2, 0);
expect(result).toBe(2000);
});
});
単体テストの目的
- ロジックの正確性を確認:計算、変換、判定などのロジックが正しいか
- エッジケースの検証:境界値や特殊な入力での動作確認
- リファクタリングの安全性:コード変更時の影響を即座に検知
結合テスト(Integration Test)
概要
結合テストは、複数のモジュールやコンポーネントの連携をテストします。
| 項目 | 詳細 |
|---|---|
| テスト範囲 | 複数のモジュール、APIとの連携 |
| 実行速度 | 中程度(秒単位) |
| テスト数 | 中程度(数十〜数百) |
| 依存関係 | 実際の依存関係を使用 |
結合テストの例
// APIとデータベースの連携テスト
import { test, expect } from '@playwright/test';
test.describe('商品API統合テスト', () => {
test('商品を登録してデータベースから取得できる', async ({ request }) => {
// 1. 商品を登録(POST)
const createResponse = await request.post('/api/products', {
data: {
name: 'ノートPC',
price: 100000,
stock: 10
}
});
expect(createResponse.ok()).toBeTruthy();
const createdProduct = await createResponse.json();
// 2. 登録した商品を取得(GET)
const getResponse = await request.get(`/api/products/${createdProduct.id}`);
expect(getResponse.ok()).toBeTruthy();
const product = await getResponse.json();
// 3. データが正しく保存されているか確認
expect(product.name).toBe('ノートPC');
expect(product.price).toBe(100000);
expect(product.stock).toBe(10);
});
test('在庫を更新してデータベースに反映される', async ({ request }) => {
// 商品作成
const createResponse = await request.post('/api/products', {
data: { name: 'マウス', price: 3000, stock: 50 }
});
const product = await createResponse.json();
// 在庫更新
const updateResponse = await request.patch(`/api/products/${product.id}`, {
data: { stock: 45 }
});
expect(updateResponse.ok()).toBeTruthy();
// 更新後のデータを確認
const getResponse = await request.get(`/api/products/${product.id}`);
const updatedProduct = await getResponse.json();
expect(updatedProduct.stock).toBe(45);
});
});
結合テストの目的
- モジュール間の連携確認:複数のコンポーネントが正しく連携するか
- API契約の検証:APIの仕様通りにデータのやり取りができるか
- データフローの確認:データが正しく流れ、変換されるか
E2Eテスト(End-to-End Test)
概要
E2Eテストは、ユーザーの視点で、システム全体をテストします。
| 項目 | 詳細 |
|---|---|
| テスト範囲 | システム全体(UI、API、DB、外部サービス) |
| 実行速度 | 遅い(数秒〜数十秒/テスト) |
| テスト数 | 少数(数十程度) |
| 依存関係 | すべての実際の依存関係を使用 |
E2Eテストの例
import { test, expect } from '@playwright/test';
test.describe('ECサイト購入フロー', () => {
test('商品を検索してカートに追加し、購入完了まで', async ({ page }) => {
// 1. ログイン
await page.goto('https://example.com/login');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page.locator('#user-menu')).toBeVisible();
// 2. 商品検索
await page.fill('#search-input', 'ノートPC');
await page.click('#search-button');
await expect(page.locator('.search-results')).toBeVisible();
// 3. 商品詳細ページへ移動
await page.click('.product-card:first-child');
await expect(page.locator('#product-title')).toContainText('ノートPC');
// 4. カートに追加
await page.click('#add-to-cart');
await expect(page.locator('#cart-count')).toContainText('1');
// 5. カート確認
await page.click('#cart-icon');
await expect(page.locator('.cart-item')).toHaveCount(1);
// 6. 購入手続き
await page.click('#checkout-button');
await page.fill('#shipping-address', '東京都渋谷区...');
await page.fill('#card-number', '4111111111111111');
await page.click('#purchase-button');
// 7. 購入完了確認
await expect(page.locator('#order-success')).toBeVisible();
await expect(page.locator('#order-number')).toBeVisible();
});
});
E2Eテストの目的
- ユーザー体験の検証:実際のユーザー操作フローが正常に動作するか
- システム全体の動作確認:すべてのコンポーネントが連携して動作するか
- 重要なビジネスシナリオの保護:売上に直結する機能が壊れていないか
[!WARNING] E2Eテストの注意点
E2Eテストは実行時間が長く、環境の影響を受けやすいため、すべてのケースをE2Eで書くのは非効率です。重要なユーザーフローに絞って実装しましょう。
各テストレベルの比較
| 項目 | 単体テスト | 結合テスト | E2Eテスト |
|---|---|---|---|
| テスト範囲 | 関数、クラス | モジュール間連携 | システム全体 |
| 実行速度 | 非常に高速 | 中程度 | 遅い |
| テスト数 | 多い | 中程度 | 少ない |
| 失敗時の原因特定 | 容易 | 中程度 | 困難 |
| メンテナンスコスト | 低い | 中程度 | 高い |
| 環境依存 | なし | あり | 高い |
| 実装コスト | 低い | 中程度 | 高い |
テスト戦略の実例
実際のプロジェクトでは、これらのテストをバランスよく組み合わせます。
ECサイトの例
// 【単体テスト】価格計算ロジック(高速・多数)
test('消費税計算', () => {
expect(calculateTax(1000, 0.1)).toBe(100);
});
// 【結合テスト】カートAPI(中速・中程度)
test('カートに商品を追加するAPI', async ({ request }) => {
const response = await request.post('/api/cart/add', {
data: { productId: 123, quantity: 2 }
});
expect(response.ok()).toBeTruthy();
});
// 【E2Eテスト】購入フロー全体(低速・少数)
test('商品購入フロー', async ({ page }) => {
// ログイン → 検索 → カート追加 → 購入完了
// ※重要なビジネスフローのみ
});
推奨される配分
プロジェクト規模別のテスト配分例:
小規模プロジェクト:
- 単体テスト:30件
- 結合テスト:10件
- E2Eテスト:5件
中規模プロジェクト:
- 単体テスト:200件
- 結合テスト:50件
- E2Eテスト:20件
大規模プロジェクト:
- 単体テスト:1000件
- 結合テスト:200件
- E2Eテスト:50件
[!TIP] テストの優先順位
- まず重要なE2Eテストを書いて、主要フローを保護
- 次に複雑なロジックの単体テストを追加
- 最後にモジュール間の連携を結合テストで確認
この順序で進めると、効率的にカバレッジを向上できます。
まとめ
- 単体テスト:関数やクラス単位の小さなテスト。高速で多数実装
- 結合テスト:モジュール間の連携テスト。中程度の速度と数
- E2Eテスト:システム全体のテスト。低速だが重要なフローを保護
- テストピラミッドに従い、バランスの取れたテスト戦略を立てる
- それぞれのテストレベルには明確な目的があり、適切に使い分けることが重要
次のチャプターでは、テストケースの書き方について学びます。