Red-Green-Refactor: 3 拍のサイクルと 3 つの実装戦略
TDD のサイクルは 3 拍です。テストを書いて失敗させ、通し、整える。図にすると単純ですが、単純なぶん各拍で何を許すかが決まっていないと、形だけの往復になります。
この章は 3 拍それぞれの線引きを扱います。あわせて、Red から Green へ渡るときに選べる 3 つの実装戦略を見ます。
この章で学ぶこと
- 各拍でやってよいことと、やってはいけないこと
- 失敗を目で見ないと何を見落とすか
- Green へ渡る 3 つの戦略と、その使い分けの条件
01 TDD とは何か を読んでいることを前提にします。
この章で扱わないこと
| 観点 | 参照先 |
|---|---|
| マッチャーの一覧と非同期テストの書き方 | TypeScript 開発ツールチェーン |
| Arrange-Act-Assert という書式、テスト名の付け方 | テスト戦略 |
| リファクタリングの個々の手順 | リファクタリングの実践 |
3 拍のサイクル
| 拍 | すること | してはいけないこと |
|---|---|---|
| Red | 失敗するテストを 1 つ書き、落ちることを確認する | 複数のテストを同時に書く。実装を先に触る |
| Green | そのテストを通す。手段は問わない | 次のテストのための実装を先回りする |
| Refactor | 重複を消し、名前を整える | 振る舞いを変える。テストを書き足す |
拍をまたぐときは、必ずテストを走らせます。いまどの拍にいるかが分からなくなったら、テストを走らせて全部緑かどうかを見れば決まります。全部緑なら Refactor か次の Red、1 つ赤なら Green です。
Red: 失敗を目で見る
書いたテストを走らせて、実際に落ちることを確認します。頭の中で「これは落ちるはずだ」と判断して次へ進みません。
落ちるところを見ないと、次の 3 つを見落とします。
テストが実行されていない場合。 ファイル名の規約から外れている、テストを登録する関数の外に書いてしまった、フィルタが効いている。どれも「緑」に見えます。1 本も走っていないので当然です。
アサーションが何も検証していない場合。 比較の左右を取り違えた、期待値を変数に入れたまま比較を書き忘れた。実装を空にしても通るテストになります。
期待値が実装と同じ誤りを含んでいる場合。 これは落ちるところを見ても気づけないことがありますが、少なくとも「落ちてから通った」という一往復は確認できます。
落ち方も見ます。期待した理由で落ちているかが大事です。まだ存在しない関数を呼んでいるなら型エラーや未定義エラーで落ちますが、それは「仕様を満たしていないから落ちた」のとは違います。関数の器だけ先に作り、中身が空の状態で落とすと、理由が仕様側に寄ります。
// まだ中身は無いが、器はある
function longestWord(array $words): string
{
return '';
}
export function longestWord(words: string[]): string {
return ''
}
この状態でテストを書くと、「空文字が返ってきた。期待は別の値」という落ち方になります。
Green: 通すことだけを考える
Green でやることは 1 つです。いま赤いテストを緑にする。それ以外はしません。
きれいに書こうとしないでください。汚くても、定数を返すだけでも構いません。重複が生まれても構いません。整えるのは次の拍です。
ここで多いのが、次のテストを見越した実装を書いてしまうことです。「どうせ後で一般化するのだから、今のうちに書いておこう」と考えた瞬間、テストが要求していないコードが生まれます。そのコードを要求するテストは存在しないので、後から壊れても誰も気づけません。
Refactor: 重複を消す
全部緑の状態で、コードの形を整えます。振る舞いを変えません。変えていないことは、走らせて全部緑のままであることで確かめます。
消す対象は重複です。テストとコードの間の重複も含みます。「テストに書いた期待値が、実装にそのまま定数として書いてある」状態は重複で、この章の後半で扱う Triangulation を使う合図になります。
この拍ではテストを書き足しません。書き足したくなったら、それは次の Red です。
Green へ渡る 3 つの戦略
赤を緑にする書き方には 3 つあります1。
Fake It
期待値をそのまま返します。 最も小さい一歩です。
function longestWord(array $words): string
{
return 'banana';
}
テストは通ります。そして、テストに書いた 'banana' と実装の 'banana' という明らかな重複が残ります。この重複が、次に何をすべきかを教えます。
Obvious Implementation
本物の実装をそのまま打ちます。 何を書けばよいか分かっていて、手が止まらないときに使います。
function longestWord(array $words): string
{
$longest = '';
foreach ($words as $word) {
if (mb_strlen($word) > mb_strlen($longest)) {
$longest = $word;
}
}
return $longest;
}
速い代わりに、間違えたときの戻り幅が大きくなります。打った実装が想定と違う落ち方をしたら、Fake It まで戻ります。
Triangulation
例が 2 つ以上そろってから一般化します。 3 つの中で最も歩幅が小さい進め方です。
1 例目では Fake It で定数を返します。2 例目を足すと、定数では両方を満たせなくなります。ここで初めて、両方を説明できる形へ一般化します。
// 1 例目: 定数で通る
test('最も長い単語を返す', function () {
expect(longestWord(['apple', 'banana']))->toBe('banana');
});
// 2 例目を足すと定数では通らない
test('先頭が最長でも返せる', function () {
expect(longestWord(['strawberry', 'fig']))->toBe('strawberry');
});
it('最も長い単語を返す', () => {
expect(longestWord(['apple', 'banana'])).toBe('banana')
})
it('先頭が最長でも返せる', () => {
expect(longestWord(['strawberry', 'fig'])).toBe('strawberry')
})
2 例目で「先頭の要素を返す」も「末尾の要素を返す」も否定されます。残るのは「長さで比べる」だけです。一般化の形が、例の方から決まります。
戦略の選び方
| 状況 | 選ぶもの |
|---|---|
| 書くべきコードが見えていて、手が止まらない | Obvious Implementation |
| 書き方は分かるが、設計に迷いがある | Fake It から始める |
| どう一般化すべきか見当がつかない | Triangulation |
| Obvious Implementation を打ったら予想外に落ちた | Fake It まで戻る |
実際には 3 つを行き来します。慣れた領域では Obvious Implementation で進み、見通しが悪くなったら歩幅を縮めます。歩幅は固定するものではなく、迷いの量に合わせて変えるものです。
Triangulation は常用する技法ではありません。一般化の形が見えているのに 2 例目を待つのは、手数が増えるだけです。使うのは、変化する軸が何なのか自分でも言えないときです。
まとめ
- 3 拍それぞれに、してよいこととしてはいけないことがあります。迷ったらテストを走らせれば、いまどの拍かが決まります
- Red では落ちるところを目で見ます。テストが走っていない、何も検証していない、という状態はどちらも緑に見えます
- Green では通すことだけをします。次のテストを見越した実装は、それを要求するテストが無いので守られません
- Refactor では振る舞いを変えず、テストも書き足しません
- Green へ渡る戦略は 3 つあり、迷いの量に応じて歩幅を変えます
次に読む
- 03 テストが設計を圧す — テストが書きにくいとき、何を直すかの判断を扱います
- 06 ドメインロジックから始める — このサイクルを PHP で 1 周します