テストケース設計技法
ストーリー
「テストを書くとき、なんとなく値を選んでないか?」
松本先輩が問いかけた。
「えっと......適当に100とか200とか入れてます」
「それだとバグを見逃すことがある。テストケース設計には 体系的な技法がある。これを使えば、少ないテストケースで 多くのバグを見つけられるようになる」
1. 境界値分析(Boundary Value Analysis)
バグは境界値(ちょうどの値やその前後)で発生しやすいという経験則に基づく手法です。
基本的な考え方
仕様: 1〜100 の範囲の数値を受け付ける
境界値:
←── 範囲外 ──→ ←──── 範囲内 ────→ ←── 範囲外 ──→
... -1 0 │ 1 2 ... 99 100 │ 101 102 ...
テストすべき値:
0 (範囲外の境界)
1 (範囲内の境界 - 最小値)
2 (範囲内の境界 + 1)
99 (範囲内の境界 - 1)
100 (範囲内の境界 - 最大値)
101 (範囲外の境界)
実践例:年齢バリデーション
typescript
// 仕様: 0歳〜150歳を有効とする
function validateAge(age: number): boolean {
return age >= 0 && age <= 150;
}
// 境界値分析に基づくテスト
describe('validateAge - 境界値分析', () => {
// 下限の境界
it('age = -1 は無効', () => expect(validateAge(-1)).toBe(false));
it('age = 0 は有効', () => expect(validateAge(0)).toBe(true));
it('age = 1 は有効', () => expect(validateAge(1)).toBe(true));
// 上限の境界
it('age = 149 は有効', () => expect(validateAge(149)).toBe(true));
it('age = 150 は有効', () => expect(validateAge(150)).toBe(true));
it('age = 151 は無効', () => expect(validateAge(151)).toBe(false));
});2. 同値分割(Equivalence Partitioning)
入力値を「同じ振る舞いをするグループ(同値クラス)」に分割し、各グループから代表値を選んでテストする手法です。
基本的な考え方
仕様: 成績判定
90〜100点: A
70〜89点: B
50〜69点: C
0〜49点: D
それ以外: エラー
同値クラス:
[-∞, -1] → エラー(無効クラス1)
[0, 49] → D(有効クラス1)
[50, 69] → C(有効クラス2)
[70, 89] → B(有効クラス3)
[90, 100] → A(有効クラス4)
[101, +∞] → エラー(無効クラス2)
各クラスの代表値: -5, 30, 60, 80, 95, 120
実践例
typescript
function getGrade(score: number): string {
if (score < 0 || score > 100) throw new Error('Invalid score');
if (score >= 90) return 'A';
if (score >= 70) return 'B';
if (score >= 50) return 'C';
return 'D';
}
// 同値分割に基づくテスト
describe('getGrade - 同値分割', () => {
it('負の値はエラー', () => {
expect(() => getGrade(-5)).toThrow('Invalid score');
});
it('0-49点はD', () => expect(getGrade(30)).toBe('D'));
it('50-69点はC', () => expect(getGrade(60)).toBe('C'));
it('70-89点はB', () => expect(getGrade(80)).toBe('B'));
it('90-100点はA', () => expect(getGrade(95)).toBe('A'));
it('101以上はエラー', () => {
expect(() => getGrade(120)).toThrow('Invalid score');
});
});3. デシジョンテーブル(Decision Table)
複数の条件の組み合わせによって結果が変わる場合に、全パターンを漏れなくテストする手法です。
基本的な考え方
仕様: 送料の計算
条件1: 会員かどうか
条件2: 注文金額が5,000円以上か
条件3: 離島かどうか
デシジョンテーブル:
┌──────────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 条件 │ R1 │ R2 │ R3 │ R4 │ R5 │ R6 │ R7 │ R8 │
├──────────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ 会員 │ Y │ Y │ Y │ Y │ N │ N │ N │ N │
│ 5000円以上│ Y │ Y │ N │ N │ Y │ Y │ N │ N │
│ 離島 │ Y │ N │ Y │ N │ Y │ N │ Y │ N │
├──────────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ 送料 │500 │ 0 │1000│500 │1000│500 │1500│800 │
└──────────┴────┴────┴────┴────┴────┴────┴────┴────┘
実践例
typescript
function calculateShipping(
isMember: boolean,
orderAmount: number,
isRemoteIsland: boolean,
): number {
const BASE_SHIPPING = 800;
let shipping = BASE_SHIPPING;
if (isMember) shipping -= 300;
if (orderAmount >= 5000) shipping -= 300;
if (isRemoteIsland) shipping += 500;
return Math.max(shipping, 0);
}
// デシジョンテーブルに基づくテスト
describe('calculateShipping - デシジョンテーブル', () => {
it('R1: 会員 + 5000円以上 + 離島 → 500円', () => {
expect(calculateShipping(true, 6000, true)).toBe(700);
});
it('R2: 会員 + 5000円以上 + 通常 → 0円', () => {
expect(calculateShipping(true, 6000, false)).toBe(200);
});
it('R4: 会員 + 5000円未満 + 通常 → 500円', () => {
expect(calculateShipping(true, 3000, false)).toBe(500);
});
it('R6: 非会員 + 5000円以上 + 通常 → 500円', () => {
expect(calculateShipping(false, 6000, false)).toBe(500);
});
it('R8: 非会員 + 5000円未満 + 通常 → 800円', () => {
expect(calculateShipping(false, 3000, false)).toBe(800);
});
// 全8パターンをテスト
});技法の使い分け
| 技法 | 最適な場面 | 例 |
|---|---|---|
| 境界値分析 | 数値の範囲チェック | 年齢、金額、数量 |
| 同値分割 | 入力値のグループ化 | 成績判定、カテゴリ分類 |
| デシジョンテーブル | 複数条件の組み合わせ | 料金計算、権限制御 |
組み合わせて使う
実際のテスト設計では、これらの技法を組み合わせて使います。
1. 同値分割で入力のグループを特定
2. 各グループの境界値を分析
3. 条件の組み合わせをデシジ ョンテーブルで整理
4. テストケースを作成
まとめ
| 技法 | 目的 | ポイント |
|---|---|---|
| 境界値分析 | 境界付近のバグを検出 | ちょうどの値、1つ前、1つ後 |
| 同値分割 | 効率的なテストケース選定 | 各グループの代表値を選ぶ |
| デシジョンテーブル | 条件の組み合わせを網羅 | 全パターンを表で整理 |
チェックリスト
- 境界値分析の手法を使ってテストケースを設計できる
- 同値分割の手法で入力値をグループ化できる
- デシジョンテーブルを作成して全パターンを網羅できる
- 3つの技法を組み合わせて使う方法を理解した
次のステップへ
テストケース設計技法を学んだら、次はTDD(テスト駆動開発)の基本を学びます。テストを先に書く開発手法の考え方を理解しましょう。
推定読了時間: 25分