メインコンテンツまでスキップ

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 つあり、迷いの量に応じて歩幅を変えます

次に読む

Footnotes

  1. 3 つの戦略は Kent Beck の『Test-Driven Development: By Example』(2002) が示したものです。本章の説明は同書の逐語ではなく、広く流通している理解にもとづきます。