Green 🟢 - テストを通過させる効率的アプローチ
TDDプロセスでは、テストリスト(TODOリスト)から1つのテストケースを選び、そのテストをパスさせるための実装を行います。本記事では、テストをパスさせるための効率的な実装アプローチについて解説します。
TDD における「実装」とは?
TDDプロセスのうち「実装」のステップで使われるのが、以下の 3つの典型的なアプローチです。
手法 | 内容 | メリット | 使う場面 |
---|---|---|---|
明確な実装 | 最初から正解を書く | 時間効率がよく、簡潔 | ロジックが明白なとき |
仮実装 | 動かすためだけの一時的なコード | 最小実装を強制できる | 最小限から始めたいとき |
三角測量 | テストを増やして一般化へ導く | テストが導く設計になる | 本格的なロジックに向かう途中段階 |
これらは状況や問題の複雑さに応じて使い分けることで、効率的かつ堅牢なTDDプロセスを実現することができます。
実際の開発では、これらのそれぞれのアプローチを組み合わせて使用することが一般的です。
明確な実装(Obvious Implementation)
テストを通過させるために、直接的かつ素直な方法でコードを書くアプローチです。
解決策が明白で、どのように実装すべきかが明らかな場合に使用します。 すでに答えが明確で、そのまま正解をストレートに書く実装です。
特徴
- 問題の解決方法が明確である場合に使用
- 単純な関数やロジックに適している
- 迅速に実装できる
使うタイミング
- ロジックがシンプルで、考えるまでもないとき
- 例えば add(2, 3) → 5 のような、仕様が直感的な場合
例
// テスト
test('2つの数値を足し算する', () => {
expect(add(2, 3)).toBe(5);
});
// 明確な実装
function add(a: number, b: number): number {
return a + b;
}
仮実装(Fake implementation / Sham implementation)
一時的に「動くけど中身が本物ではない」コードを書く方法です。 コードでまずベタ書きの値を使い、実装を進めつつ、徐々に変数に置き換えるなど、ハードコーディングや条件付きで値を返すことが多いです。
テストを通過させるために、一時的な固定値やハードコードした値を返すシンプルな実装を行うアプローチです。
これは、まず最も単純な方法でテストを通過させ、その後リファクタリングによって本来の実装に置き換えていく戦略です。
特徴
- 最も単純な方法でテストを通す
- ハードコードした値を返すことが多い
- 徐々に本来の実装に進化させる
使うタイミング
明確な実装を続けている中で、予期しないレッドバーを目にした場合、仮実装に切り替える。
使う理由
- TDD のサイクルを止めずに進めるため
- 余計なロジックを書くのを防ぎ、「最低限」のコードで済ませる
例
以下のように、add(2, 3) に対応するよう「5」を返すだけ。
// テスト
test('2つの数値を足し算する', () => {
expect(add(2, 3)).toBe(5);
});
// 仮実装(最初のステップ)
function add(a: number, b: number): number {
return 5; / テストが通るように、仮で決め打ちし、ハードコードした値を返す
}
// リファクタリング後の本来の実装
function add(a: number, b: number): number {
return a + b;
}
三角測量(Triangulation)
一般的な実装に進む前に、複数のテストケースを作成することで、より確実な実装を導き出すアプローチです。
一つのテストケースだけでは偶然通過する可能性がありますが、複数のテストケースを使用することで、より堅牢な実装を見つけ出せます。
複数の異なる観測点から対象の位置を正確に特定する「測量手法」になぞらえて、\複数のテストを通じて「正しい実装位置(一般化)」を導き出すという意味です。
複数のテストケースを増やして、一般化した実装へ導きます。
最初は仮実装で通し、異なる値のテストケースを追加して、共通化できる実装へ進化させていきます。
特徴
- 複数のテストケースを使用して実装を導き出す
- 一般化された解決策を見つけるために使用
- 偶然通過する可能性を減らす
例
// 最初のテスト
test('2と3の足し算は5になる', () => {
expect(add(2, 3)).toBe(5);
});
// 2つ目のテスト
test('5と7の足し算は12になる', () => {
expect(add(5, 7)).toBe(12);
});
// 3つ目のテスト
test('0と0の足し算は0になる', () => {
expect(add(0, 0)).toBe(0);
});
// 三角測量によって導き出された実装
function add(a: number, b: number): number {
return a + b;
}
どの手法をいつ使うべきか?
判断軸 | 明確な実装 | 仮実装 | 三角測量 |
---|---|---|---|
問題の複雑さ | 低い | 低〜中 | 中〜高 |
実装の見通し | はっきりしている | 一部見えている | あいまいで未知が多い |
テストの数 | 少数で済む | 1つで様子を見る | 増やして一般化する |
TDDの段階 | 初期または明確な部分 | 初期または不確実なとき | 実装を進化させるとき |
三角測量における注意点
- テストケースを追加するときは、「境界値」「異常系」など、意味のある観測点を意識するとよい。
- 不要な冗長テストにならないよう注意(実装が一般化された後は、類似のテストは削除してよい)。
- 2つ目のテストケースで実装が不十分と判断したら、すぐに3つ目を追加して一般化を強化する。
アンチパターンと注意点
アンチパターン | 説明 | 対策 |
---|---|---|
「最初から完璧な実装」を目指す | Red→Greenのプロセスを無視して一気に実装してしまう | 必ずRed→Green→Refactorを守る |
仮実装から脱却できない | べた書きが残り、本来の実装に進化できない | 新しいテストで一般化を促す |
三角測量のテストが増えすぎる | 類似ケースを無意味に追加してしまう | 意味の異なる値を使い、過剰なテストは整理する |