本章の方針
前章で最初の Red-Green-Refactor を回した。6サイクルで Money クラスが育った。だが、正直に言おう。前章のテストの順序は筆者が事前に設計した順序だった。
実際の開発では、「次に何をテストすべきか」が分からないことの方が多い。この「次の一手がわからない」感覚が、TDD 挫折の最大の原因だ。
本章では、「次のテスト」を選ぶための具体的な技法を3つ紹介する。
技法1:テストリスト
Kent Beck が『TDD by Example』で提唱した最もシンプルな方法だ。
やり方
実装を始める前に、思いつくテストを全て書き出す。リスト形式でいい。コードにする必要はない。
Money のテストリスト(思いつくまま):
□ 同じ金額・同じ通貨は等しい
□ 金額が違えば等しくない
□ 通貨が違えば等しくない
□ 負の金額は作れない
□ 0円は作れる
□ 加算できる
□ 異なる通貨は加算できない
□ 減算できる
□ 異なる通貨は減算できない
□ 減算で負になるとエラー
□ 比較できる(大小)
□ Infinity は作れない
□ NaN は作れない
□ 通貨コードが不正なら作れない
□ 小数点以下の扱い(0.1 + 0.2 問題)
順序を決める
書き出したら、簡単なものから順に並べ替える。
順序付けの原則:
1. 簡単なもの(自信があるもの)から始める
2. 一歩で実装が進むものを優先する
3. 複雑なもの・迷うものは後回しにする
理由:
・簡単なテストで「動く状態」を早く作る
・成功体験がリズムを生む
・後のテストは、先のテストの上に積み上がる
テストリストは生き物
テストリストは書いたら終わりではない。
開発中のテストリスト操作:
・実装中に気づいた新しいケースを追加する → □ 追加
・不要と判断したケースを削除する → ✗ 削除
・完了したケースにチェックを入れる → ✓ 完了
・今は手をつけないケースに印をつける → △ 保留
前章の Money は、テストリストの上半分をこなしたことになる。下半分(減算、比較、小数点)は、必要になるまで保留だ。
技法2:三角測量(Triangulation)
「テストを1本追加するたびに、実装を少しずつ汎用化していく」技法。
具体例:Money の比較
Money に大小比較を追加する場面を考える。
// 最初のテスト
test('金額が大きい方がgreaterThanでtrueを返す', () => {
const large = Money.of(1000, 'JPY')
const small = Money.of(500, 'JPY')
expect(large.greaterThan(small)).toBe(true)
})
Green にする最小のコードは?
// ❌ 最小だが「ズル」な実装
greaterThan(other: Money): boolean {
return true // テストは通るが、常に true を返すだけ
}
テストが1本しかないと、この「ズル」な実装でも通ってしまう。ここで三角測量が効く。2本目のテストを追加する。
// 2本目のテスト:別の角度からの検証
test('金額が小さい方がgreaterThanでfalseを返す', () => {
const large = Money.of(1000, 'JPY')
const small = Money.of(500, 'JPY')
expect(small.greaterThan(large)).toBe(false)
})
これで return true は通らなくなる。「ズル」ができなくなったので、正しい実装を書く。
greaterThan(other: Money): boolean {
if (this.currency !== other.currency) {
throw new Error('通貨が一致しません')
}
return this.amount > other.amount
}
三角測量の使いどころ
三角測量が有効な場面:
・実装の方向が明確でないとき
・「最小の実装」がハードコードになりそうなとき
・1本のテストでは仕様の意図が伝わらないとき
三角測量が不要な場面:
・実装の方向が明確で、自信があるとき
→ 素直に正しい実装を書いてよい
三角測量は「毎回やるべきルール」ではない。実装に自信がないとき、あるいは「ズル」な実装に引きずられそうなときに使う道具だ。
技法3:境界値分析
テストケースの中で最も効果的にバグを見つけるのは、境界値のテストだ。
境界値の見つけ方
Money を例に、境界値を体系的に洗い出す。
金額(amount)の境界値:
・0 → 許容される最小値
・-1 → 拒絶される最大値(0のすぐ下)
・Infinity → 数値の上限
・NaN → 数値でない
・0.1 + 0.2 → 浮動小数点の罠
通貨(currency)の境界値:
・'JPY' → 正常な3文字コード
・'' → 空文字列
・'JP' → 2文字(短すぎる)
・'JPYY' → 4文字(長すぎる)
・'jpy' → 小文字
境界値テストの書き方
describe('Money 境界値', () => {
test('0円は生成できる', () => {
const zero = Money.of(0, 'JPY')
expect(zero.amount).toBe(0)
})
test('-1円は生成できない', () => {
expect(() => Money.of(-1, 'JPY')).toThrow()
})
test('空文字の通貨コードは生成できない', () => {
expect(() => Money.of(1000, '')).toThrow()
})
})
等価パーティショニングとの組み合わせ
境界値分析を等価パーティショニング(同じ振る舞いをする値をグループ化)と組み合わせると、テストの網羅性と効率を両立できる。
金額のパーティション:
┌──────────────┬──────────────┬──────────────┐
│ 負の数 │ 0以上の有限値 │ 非数値 │
│ (-∞, 0) │ [0, ∞) │ NaN, Infinity │
│ → 拒絶 │ → 受理 │ → 拒絶 │
└──────────────┴──────────────┴──────────────┘
テストすべき代表値:
負の数 → -1(境界値のすぐ下)
0以上 → 0(境界値そのもの), 1000(典型値)
非数値 → NaN, Infinity
テストの命名規則
テストリストが長くなると、テスト名が仕様書として機能する。読みやすいテスト名のパターンを紹介する。
パターン1:「〜した場合、〜になる」
// ✅ 条件と結果が明確
test('異なる通貨を加算した場合、エラーを投げる', ...)
test('負の金額で生成した場合、エラーを投げる', ...)
test('同じ金額・同じ通貨で比較した場合、等しいと判定する', ...)
パターン2:主語を明示する
// ✅ 「誰が」「何を」が明確
test('Money.of は負の金額を拒絶する', ...)
test('Money.add は異なる通貨の加算を拒絶する', ...)
test('Money.equals は金額と通貨の両方を比較する', ...)
アンチパターン:曖昧なテスト名
// ❌ 何をテストしているか分からない
test('Money テスト', ...)
test('正常系', ...)
test('エラーケース', ...)
test('add のテスト1', ...)
テスト名が曖昧だと、テストが失敗したときに「何が壊れたか」がすぐに分からない。テスト名は失敗時のエラーメッセージとしても機能することを意識する。
テストの構造:Arrange-Act-Assert
個々のテストの内部構造には、AAA(Arrange-Act-Assert) パターンが有効だ。
test('同じ通貨のMoneyを加算できる', () => {
// Arrange:テストの前提条件を準備する
const a = Money.of(1000, 'JPY')
const b = Money.of(500, 'JPY')
// Act:テスト対象の操作を実行する
const result = a.add(b)
// Assert:期待する結果を検証する
expect(result.equals(Money.of(1500, 'JPY'))).toBe(true)
})
AAA の原則:
・Arrange:1テストに1つのシナリオ
・Act :1テストに1つのアクション
・Assert :1テストに1つの検証(原則)
1テスト1アサートは理想だが、関連する複数のアサートを
1テストにまとめることは許容される(例:equals で amount と currency を両方確認)。
テストリストの実践:Subscription に向けた準備
次章で扱う Subscription(契約管理)のテストリストを、練習として作ってみよう。
Subscription のテストリスト(思いつくまま):
□ トライアルを開始できる
□ トライアルからアクティブに移行できる
□ アクティブからキャンセルできる
□ キャンセル済みからアクティブには戻れない
□ トライアルからいきなりキャンセルできる(?)
□ 期限切れのトライアルはアクティブにできない(?)
□ プランを変更できる(アクティブ時のみ?)
□ 同じプランへの変更はエラー?
? がついている項目は、仕様が不明確だ。TDD では、テストを書こうとした時点で仕様の曖昧さが発見される。これは TDD の副産物として非常に価値が高い。仕様が曖昧なまま実装を始めると、後から「これどうするんだっけ?」と手戻りが発生する。
本章のまとめ
| 技法 | 使いどころ | 効果 |
|---|---|---|
| テストリスト | 実装開始前 | テスト対象の全体像を把握。抜け漏れを防ぐ |
| 三角測量 | 実装の方向が不明確なとき | ハードコードを防ぎ、汎用的な実装に導く |
| 境界値分析 | テストケースの選定時 | 最小のテスト数で最大のバグ検出力 |
テスト設計の思考フロー:
1. テストリストで全体を俯瞰する
2. 簡単なものから順に並べる
3. 各テストで境界値を意識する
4. 実装に自信がなければ三角測量で補強する
5. 開発中にリストを更新し続ける
次章では、Money よりも複雑な題材に進む。状態を持つオブジェクト──Subscription の状態遷移を、テストで駆動してみよう。