LoginSignup
22
31

テスト駆動開発をスムーズに進めていく方法を考える

Last updated at Posted at 2023-10-28

この記事はテスト駆動開発についての筆者の学習と実践をまとめたものです。

この記事で扱わないこと

  • テスト駆動開発についての説明
  • 実務レベルのリファクタリングやテスト

この記事について

テスト駆動開発をしていると、しばらく手が止まってしまったり、大きな変更を行うことがありました。
そのようなことを全て避けることは難しいと思いますが、できるだけ行き詰まらずに進んでいくにはどうすればよいか考えてみました。
後半では考察を元に実践してみた要素を記載しています。

ポイント

  1. 小さなステップで変更する
  2. 優先順位を意識する

1. 小さなステップで変更する

小さく進めていくことは、複雑な問題を解決するにあたっては基本かもしれません。
では「小さなステップ」とはどのくらいでしょうか?

『リファクタリング(第2版)』1では、何気ない変更がリストアップされていたり(例えば”関数宣言の変更”や”ステートメントの移動”)、一見ちょっとしたリファクタリングでも多くの手順を踏んでたりします。

このように、小さなステップを認識することで、よりきめ細かいコントローラーが可能になり、次のステップへの足がかりとなることが期待できます。

2. 優先順位を意識する

では、どのようなステップを選んでいけばよいのでしょうか?

Robert C. Martin氏(ボブおじさん)は「変換には優先順位がある」と主張しています。2

この説について、もっとも新しい情報(執筆時点)は『Clean Craftsmanship』3 だと思います。
書籍の中では、次の8段階の優先順位が記載されています。(ブログから約10年を経て、12→8段階になっています)

  1. { } → Nil
  2. Nil → 固定値
  3. 固定値 → 変数
  4. 無条件 → 選択
  5. 値 → リスト
  6. 選択 → 反復
  7. 文 → 再帰
  8. 値 → 変異値

この優先順位について「あくまで説であり、この順番が常に正しいとは限らないが、優先順位はあると信じている(意訳)」とのことです。
次の一歩決めるための判断材料として利用できるのではないかと考えています。

これらのポイントを踏まえて実践してみましょう。

実践編

TDD Katas のFizzBuzzに挑戦してみましょう。

最初のタスクは次の通りです。

1から100までの数字の配列を返すプログラムを書きなさい。3の倍数は数字の代わりに "Fizz "を返し、5の倍数は "Buzz "を返す。3と5の倍数は "FizzBuzz "を返す。

分解してTODOリストにしていきましょう。

TODOリスト

  • 配列を返す
  • 配列のそれぞれの要素について、
    • 数字は、変換せずに返す
    • 3の倍数の数字は、"Fizz"に変換して返す
    • 5の倍数の数字は、"Buzz"に変換して返す
    • 3の倍数かつ5の倍数の数字は、"FizzBuzz"に変換して返す
  • 1から100までの数字

このようになりました。

課題の解釈はいろいろあると思います。(例えば、これが関数なのかクラスなのか、数字の型、など)
課題から外れない限りは「今回はこのようにやる」とします。

配列を返す

まず最初のテストを書きます。

テストコード
describe('FizzBuzz', () => {
  test('配列を返す', () => {
    expect(fizzBuzz()).toEqual([]);
  });
});

Array.isArrayでのテストをしようと思いましたが、まずは単純に値が返るところを見ていきます。
そして、プロダクトコード関数宣言だけをします。

プロダクトコード
function fizzBuzz() {}

結果はundefinedなのでレッドです。
優先順位を参考に「{ } → Nil」の変換をしましょう。

プロダクトコード
- function fizzBuzz() {}
+ function fizzBuzz() {
+   return null;
+ }

テストの結果は変わらずレッドですが、undefinedからnullになったことは確認できます。プロダクトコードの変更が正しく行えていることを確認できました。
次は「Nil → 固定値」に進みましょう。

プロダクトコード
function fizzBuzz() {
- return null;
+ return [];
}

テストこれでテストがグリーンになりました。
テストを当初考えていたArray.isArrayを使う形に変更しましょう。

テストコード
  test('配列を返す', () => {
-   expect(fizzBuzz()).toEqual([]);
+   expect(Array.isArray(fizzBuzz())).toBe(true);
  });

これもグリーンになります。しかし変更したテストが正しく機能しているか心配です。安心して次に進む前に、プロダクトコードから返す値をいったんnullに戻してテストしてみます。
レッドになったのでテストが正しいことが確認できました。これで次に進めます。

  • 配列を返す

数字は、変換せずに返す

次のTODOはこちらです。

  • 配列のそれぞれの要素について、
    • 数字は、変換せずに返す

早速テストを書きましょう。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('数字は、変換せずに返す', () => {
    const result = fizzBuzz();
    expect(result[0]).toBe(1);
  });
});

undefinedが返って、レッドになります。
単純に固定値を返してみましょう。

プロダクトコード
function fizzBuzz() {
- return [];
+ return [1];
}

これでグリーンになりました。
もう一つ観測点を増やしてみます。

テストコード
  test('数字は、変換せずに返す', () => {
    const result = fizzBuzz();
    expect(result[0]).toBe(1);
+   expect(result[1]).toBe(2);
  });

レッドになることを確認します。

プロダクトコード
function fizzBuzz() {
- return [1];
+ return [1, 2];
}

これでグリーンになりました。

  • 配列のそれぞれの要素について、
    • 数字は、変換せずに返す

3の倍数の数字は、"Fizz"に変換して返す

  • 配列のそれぞれの要素について、
    • 3の倍数の数字は、"Fizz"に変換して返す

まずはテストを書きます。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('3の倍数の数字は、Fizzに変換して返す', () => {
    const result = fizzBuzz();
    expect(result[2]).toBe('Fizz');
  });
});

テストはレッドです。

これをグリーにするように変更していきたいのですが、まず思い出したいのが、目的は「数字の配列を変換する」ということです。
現在のプロダクトコードでは変換をしていません。
そこで、まずは「固定値 → 変数」にしてみましょう。

プロダクトコード
function fizzBuzz() {
- return [1, 2];
+ const numbers = [1, 2];
+ return numbers;
}

テストはグリーンのままです。
次に数字の配列を結果の配列に変換してみましょう。

プロダクトコード
function fizzBuzz() {
   const numbers = [1, 2];
-  return numbers;
+  const result = numbers;
+  return result;
}

テストは引き続きグリーンです。これで「数字の配列を変換する」という形になりました。
それでは改めて新しいTODOに取り組んでいきましょう。

プロダクトコード
function fizzBuzz() {
- const numbers = [1, 2];
+ const numbers = [1, 2, 3];
  const result = numbers;
  return result;
}

3がそのまま変えるのでテストは引き続きレッドです。
変換の過程を細かくしていきましょう。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
- const result = numbers;
+ let result = [];
+ result.push(numbers[0]);
+ result.push(numbers[1]);
+ result.push('Fizz');
  return result;
}

これで新しいテストもグリーンになりましたが、まだ固定値を返しているだけです。
ここで「無条件 → 選択」に変換してみましょう。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
  let result = [];
  result.push(numbers[0]);
  result.push(numbers[1]);
- result.push('Fizz');
+ result.push(numbers[2] % 3 === 0 ? 'Fizz' : numbers[2]);
  return result;
}

テストはグリーンのままです。
では、この条件を他の数値に当てはめるとどうでしょうか?

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
  let result = [];
- result.push(numbers[0]);
- result.push(numbers[1]);
+ result.push(numbers[0] % 3 === 0 ? 'Fizz' : numbers[0]);
+ result.push(numbers[1] % 3 === 0 ? 'Fizz' : numbers[1]);
  result.push(numbers[2] % 3 === 0 ? 'Fizz' : numbers[2]);
  return result;
}

全てのテストがグリーンのままです。
今度は条件の重複を解決していきましょう。配列の添字が固定値であることに注目し「固定値 → 変数」の変換を行います。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
  let result = [];
- result.push(numbers[0] % 3 === 0 ? 'Fizz' : numbers[0]);
- result.push(numbers[1] % 3 === 0 ? 'Fizz' : numbers[1]);
- result.push(numbers[2] % 3 === 0 ? 'Fizz' : numbers[2]);
+ let i = 0;
+ result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
+ i++;
+ result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
+ i++;
+ result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
  return result;
}

これで変換の過程は全く同じになりました。
「選択 → 反復」の変換をしましょう。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
  let result = [];
- let i = 0;
- result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
- i++;
- result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
- i++;
- result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
+ for (let i = 0; i < numbers.length; i++) {
+   result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
+ }
  return result;
}

テストはグリーンですが、ここはmapメソッドを使うほうが良さそうです。
『リファクタリング(第2版)』2 の「パイプラインによるループの置き換え」です。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
- let result = [];
- for (let i = 0; i < numbers.length; i++) {
-   result.push(numbers[i] % 3 === 0 ? 'Fizz' : numbers[i]);
- }
+ const result = numbers.map((num) => num % 3 === 0 ? 'Fizz' : num);
  return result;
}

もはや変数resultも不要ですね。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3];
- const result = numbers.map((num) => num % 3 === 0 ? 'Fizz' : num);
- return result;
+ return numbers.map((num) => num % 3 === 0 ? 'Fizz' : num);
}
  • 配列のそれぞれの要素について、
    • 3の倍数の数字は、"Fizz"に変換して返す

5の倍数の数字は、"Buzz"に変換して返す

  • 配列のそれぞれの要素について、
    • 5の倍数の数字は、"Buzz"に変換して返す

次のルールに進むに当たって、まずはテストを書きます。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('5の倍数の数字は、Buzzに変換して返す', () => {
    const result = fizzBuzz();
    expect(result[4]).toBe('Buzz');
  });
});

テストが失敗することを確認して、まずは数字の配列に値を追加します。

プロダクトコード
function fizzBuzz() {
- const numbers = [1, 2, 3];
+ const numbers = [1, 2, 3, 4, 5];
  return numbers.map((num) => num % 3 === 0 ? 'Fizz' : num);
}

続いて、新しいルールを追加するために変更しましょう。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3, 4, 5];
- return numbers.map((num) => num % 3 === 0 ? 'Fizz' : num);
+ return numbers.map((num) => {
+   if (num % 3 === 0) return 'Fizz';
+   return num;
+ });
}

ここまでくれば、新しいルールを加えるだけです。

プロダクトコード
function fizzBuzz() {
  const numbers = [1, 2, 3, 4, 5];
  return numbers.map((num) => {
    if (num % 3 === 0) return 'Fizz';
+   if (num % 5 === 0) return 'Buzz';
    return num;
  });
}
  • 配列のそれぞれの要素について、
    • 5の倍数の数字は、"Buzz"に変換して返す

3の倍数かつ5の倍数の数字は、"FizzBuzz"に変換して返す

  • 配列のそれぞれの要素について、
    • 3の倍数かつ5の倍数の数字は、"FizzBuzz"に変換して返す

変換ルールの最後に取り掛かります。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('3の倍数かつ5の倍数の数字は、"FizzBuzz"に変換して返す', () => {
    const result = fizzBuzz();
    expect(result[5]).toBe('FizzBuzz');
  });
});

手順は先程と同じです。一気にやってしまいます。

プロダクトコード
function fizzBuzz() {
- const numbers = [1, 2, 3, 4, 5];
+ const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
  return numbers.map((num) => {
+   if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });
}
  • 配列のそれぞれの要素について、
    • 3の倍数かつ5の倍数の数字は、"FizzBuzz"に変換して返す

1から100までの数字

最後に残ったTODOに取り掛かります。

  • 1から100までの数字

100個目の要素を確認するテストを書きます。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('最後の要素 100 は Buzz を返す', () => {
    const result = fizzBuzz();
    expect(result[99]).toBe('Buzz');
  });
});

配列を固定値から変更します。
この手のよくある配列生成は既にあるものを使わせていただきましょう。(参考 4

プロダクトコード
function fizzBuzz() {
- const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+ const numbers = [...Array(100)].map((_, i) => i + 1);
  return numbers.map((num) => {
    if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });
}

これでテストが通りました。
念のため要素の数もテストしておきます。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('配列の要素数は100', () => {
    const result = fizzBuzz();
    expect(result).toHaveLength(100);
  });
});

テストはグリーンになりますが、念のため失敗することも確認しておきます。

プロダクトコード
function fizzBuzz() {
  const numbers = [...Array(100)].map((_, i) => i + 1);
+ numbers.push(101); // テスト失敗を確認するための一時的な欠陥挿入
  • 1から100までの数字

これで最初のTODOは全て完了しました。

プロダクトコード
function fizzBuzz() {
  const numbers = [...Array(100)].map((_, i) => i + 1);
  return numbers.map((num) => {
    if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });
}

この後はさらに2つのタスクに取り組みますが、内容は折りたたみにしておきます。
お急ぎの場合は「まとめ」に進んでください。

  • 数字の範囲を変更する
  • 7と11のルールを追加

数字の範囲を変更する

2つ目のタスクに取り組みます。

1から100までの数字を印刷する代わりに、範囲を変更する方法を追加する。
例:1から20までの数字、15から50までの数字。

省略

まずはTODOリストを作成しましょう。

  • 数字配列の範囲を変更できるようにする
    • 最後の値を指定できる
    • 最初の値を指定できる

最初のテストを追加します。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('1から20までのFizzBuzz配列を返す', () => {
    const expected = [1,2,"Fizz",4,"Buzz" /* ... */ ,19,"Buzz"];
    expect(fizzBuzz(20)).toEqual(expected);
  });
});

まず最後の値を指定する変数を抽出しましょう。

プロダクトコード
function fizzBuzz() {
- const numbers = [...Array(100)].map((_, i) => i + 1);
+ const end = 100;
+ const numbers = [...Array(end)].map((_, i) => i + 1);

次はその変数を引数にします。
これまでのテストを通すために、デフォルト値を設定しておきましょう。

プロダクトコード
-function fizzBuzz() {
- const end = 100;
+function fizzBuzz(end = 100) {
  const numbers = [...Array(end)].map((_, i) => i + 1);

これでテストが通りました。

  • 数字配列の範囲を変更できるようにする
    • 最後の値を指定できる
    • 最初の値を指定できる

次のタスクのテストを書きます。

テストコード
describe('FizzBuzz', () => {
  // ...
  test('15から50までのFizzBuzz配列を返す', () => {
    const expected = ["FizzBuzz",16,17 /* ... */,49,"Buzz"];
    expect(fizzBuzz(15, 50)).toEqual(expected);
  });
});

こちらも同じく変数として抽出してから、引数に移動します。

プロダクトコード
-function fizzBuzz(end = 100) {
- const numbers = [...Array(end)].map((_, i) => i + 1);
+function fizzBuzz(start = 1, end = 100) {
+ const numbers = [...Array(end - start + 1)].map((_, i) => i + start);

1つ前のテストがレッドになりました。引数が変わったのでテストを修正しましょう。
(より小さなステップで進めるなら、まずは引数startを後ろに加え、後で順番を入れ替えます)

テストコード
  test('1から20までのFizzBuzz配列を返す', () => {
    const expected = [1,2,"Fizz",4,"Buzz" /* ... */ ,19,"Buzz"];
-   expect(fizzBuzz(20)).toEqual(expected);
+   expect(fizzBuzz(1, 20)).toEqual(expected);
  });

再び全てテストがグリーンになりました。

  • 数字配列の範囲を変更できるようにする
    • 最後の値を指定できる
    • 最初の値を指定できる

これで完了としてしまいたいですが、数字を生成する処理は一見ではわからないかもしれません。
関数として抽出し、適切な名前を与えましょう。

プロダクトコード
function fizzBuzz(start = 1, end = 100) {
- const numbers = [...Array(end - start + 1)].map((_, i) => i + start);
+ const numbers = intRange(start, end);
  // ...
  }

+function intRange(start, end) {
+ return [...Array(end - start + 1)].map((_, i) => i + start);
+}

これで完了です。

  • 数字配列の範囲を変更できるようにする
プロダクトコード
function fizzBuzz(start = 1, end = 100) {
  const numbers = intRange(start, end);
  return numbers.map((num) => {
    if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });
}
function intRange(start, end) {
  return [...Array(end - start + 1)].map((_, i) => i + start);
}

7と11のルールを追加

3つ目のタスクに取り組みます。

7と11のルールを追加。7の倍数は "Foo "を返し、11の倍数は "Boo "を返し、両者の倍数は "FooBoo "を返す。

省略

まずはTODOリストを作成します。

  • 7の倍数の数字は、"Foo"に変換して返す
  • 11の倍数の数字は、"Boo"に変換して返す
  • 7の倍数かつ11の倍数の数字は、"FooBoo"に変換して返す

特に記載はないのですが、FizzBuzzのルールに追加する形とするので、次の項目も追加します。

  • 3の倍数かつ7の倍数の数字は、"FizzFoo"に変換して返す
  • 5の倍数かつ7の倍数の数字は、"BuzzFoo"に変換して返す
  • 3の倍数かつ11の倍数の数字は、"FizzBoo"に変換して返す
  • 5の倍数かつ11の倍数の数字は、"BuzzBoo"に変換して返す

まず既存のルールを整理しましょう。

プロダクトコード
  return numbers.map((num) => {
    if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });

「3の倍数かつ5の倍数」のルールは、2つのルールの組み合わせに見えます。
そのように書き換えてみます。

プロダクトコード
  return numbers.map((num) => {
-   if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
-   if (num % 3 === 0) return 'Fizz';
-   if (num % 5 === 0) return 'Buzz';
-   return num;
+   let result = '';
+   if (num % 3 === 0) result += 'Fizz';
+   if (num % 5 === 0) result += 'Buzz';
+   if (result === '') result = num;
+   return result;
  });

さらに倍数のルールの差分を抽出してみます。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
-   if (num % 3 === 0) result += 'Fizz';
-   if (num % 5 === 0) result += 'Buzz';
+   let multiples;
+   let replace;
+   multiples = 3;
+   replace = 'Fizz';
+   if (num % multiples === 0) result += replace;
+   multiples = 5;
+   replace = 'Buzz';
+   if (num % multiples === 0) result += replace;
    if (result === '') result = num;
    return result;
  });

これで条件と変換の部分は共通になりました。関数として抽出します。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
   let multiples;
   let replace;
   multiples = 3;
   replace = 'Fizz';
-   if (num % multiples === 0) result += replace;
-   multiples = 5;
-   replace = 'Buzz';
-   if (num % multiples === 0) result += replace;
+   result = multiplesRule(3, 'Fizz', num, result);
+   result = multiplesRule(5, 'Buzz', num, result);
    if (result === '') result = num;
    return result;
  });
  // ...

+function multiplesRule(multiples, replace, num, carry) {
+ if (num % multiples === 0) carry += replace;
+ return carry;
+}

数字をそのまま返す部分も関数に抽出してみます。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
    result = multiplesRule(3, 'Fizz', num, result);
    result = multiplesRule(5, 'Buzz', num, result);
-   if (result === '') result = num;
+   result = passThroughRoule(num, result);
    return result;
  });
  // ...

+function passThroughRoule(num, carry) {
+ if (carry === '') carry = num;
+ return carry;
+}

倍数と文字列の指定をなんとかすれば、(number, string) => stringの形に統一され、「選択 → 反復」に出来そうです。
multiplesRuleを関数が戻り値になるように変更してみましょう。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
-   result = multiplesRule(3, 'Fizz', num, result);
-   result = multiplesRule(5, 'Buzz', num, result);
+   let rule;
+   rule = multiplesRule(3, 'Fizz');
+   result = rule(num, result);
+   rule = multiplesRule(5, 'Buzz');
+   result = rule(num, result);
    result = passThroughRoule(num, result);
    return result;
  });
  // ...

-function multiplesRule(multiples, replace, num, carry) {
- if (num % multiples === 0) carry += replace;
- return carry;
+function multiplesRule(multiples, replace) {
+ return (num, carry) => {
+   if (num % multiples === 0) carry += replace;
+   return carry;
  }
}

さらにpassThroughRoule関数も同じように変更しましょう。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
    let rule;
    rule = multiplesRule(3, 'Fizz');
    result = rule(num, result);
    rule = multiplesRule(5, 'Buzz');
    result = rule(num, result);
-   result = passThroughRoule(num, result);
+   rule = passThroughRoule;
+   result = rule(num, result);
    return result;
  });
  // ...

-function passThroughRoule(num, carry) {
- if (carry === '') carry = num;
- return carry;
-}
+const passThroughRoule =
+ (num, carry) => carry === '' ? num : carry;

これでいよいよループにできます。

プロダクトコード
  return numbers.map((num) => {
    let result = '';
-   let rule;
-   rule = multiplesRule(3, 'Fizz');
-   result = rule(num, result);
-   rule = multiplesRule(5, 'Buzz');
-   result = rule(num, result);
-   rule = passThroughRoule;
-   result = rule(num, result);
+   const rules = [
+     multiplesRule(3, 'Fizz'),
+     multiplesRule(5, 'Buzz'),
+     passThroughRoule
+   ];
+   for (let i = 0; i < rules.length; i++) {
+     result = rules[i](num, result);
+   }
    return result;
  });
  // ...

これでルールの指定とその実行が分離できました。
リファクタリングも行いましょう。このループはreduceメソッドが適しています。またmultiplesRule関数もpassThroughRoule関数と揃える形に変更しましょう。rulesも移動します。

プロダクトコード
+ const rules = [
+   multiplesRule(3, 'Fizz'),
+   multiplesRule(5, 'Buzz'),
+   passThroughRoule
+ ];
  return numbers.map((num) => {
-   let result = '';
-   const rules = [
-     multiplesRule(3, 'Fizz'),
-     multiplesRule(5, 'Buzz'),
-     passThroughRoule
-   ];
-   rules.forEach((rule) => result = rule(num, result));
-   return result;
+   return rules.reduce((carry, rule) => rule(num, carry), '');
  });
  // ...

const multiplesRule = (multiples, replace) =>
  (num, carry) => num % multiples === 0 ? carry + replace : carry;
const passThroughRoule  =
  (num, carry) => carry === ''          ? num             : carry;

さて、これでルールの追加は簡単にできるようになりました。
次は新しい変換ルールに対応していきます。

テストコード
  test('7の倍数の数字は、"Foo"に変換して返す', () => {
    const result = fizzBuzz();
    expect(result[6]).toBe('Foo');
  });

これまでのリファクタリングにより、このルールの追加は簡単です。

プロダクトコード
  const rules = [
    multiplesRule(3, 'Fizz'),
    multiplesRule(5, 'Buzz'),
+   multiplesRule(7, 'Foo'),
    passThroughRoule
  ];
  return numbers.map((num) => {
   return rules.reduce((carry, rule) => rule(num, carry), '');
  });

3かつ7、5かつ7のケースもグリーンになります。

テストコード
  test('3の倍数かつ7の倍数の数字は、"FizzFoo"に変換して返す', () => {
    const result = fizzBuzz();
    expect(result[20]).toBe('FizzFoo');
  });
  test('5の倍数かつ7の倍数の数字は、"BuzzFoo"に変換して返す', () => {
    const result = fizzBuzz();
    expect(result[34]).toBe('BuzzFoo');
  });
  • 7の倍数の数字は、"Foo"に変換して返す
  • 11の倍数の数字は、"Boo"に変換して返す
  • 7の倍数かつ11の倍数の数字は、"FooBoo"に変換して返す
  • 3の倍数かつ7の倍数の数字は、"FizzFoo"に変換して返す
  • 5の倍数かつ7の倍数の数字は、"BuzzFoo"に変換して返す
  • 3の倍数かつ11の倍数の数字は、"FizzBoo"に変換して返す
  • 5の倍数かつ11の倍数の数字は、"BuzzBoo"に変換して返す

残りのルールにも対応するのは簡単です。
それぞれのテストを書いて、ルールを追加します。

プロダクトコード
  const rules = [
    multiplesRule(3, 'Fizz'),
    multiplesRule(5, 'Buzz'),
    multiplesRule(7, 'Foo'),
+   multiplesRule(11, 'Boo'),
    passThroughRoule
  ];

これでタスクは全て完了です。

  • 7の倍数の数字は、"Foo"に変換して返す
  • 11の倍数の数字は、"Boo"に変換して返す
  • 7の倍数かつ11の倍数の数字は、"FooBoo"に変換して返す
  • 3の倍数かつ7の倍数の数字は、"FizzFoo"に変換して返す
  • 5の倍数かつ7の倍数の数字は、"BuzzFoo"に変換して返す
  • 3の倍数かつ11の倍数の数字は、"FizzBoo"に変換して返す
  • 5の倍数かつ11の倍数の数字は、"BuzzBoo"に変換して返す

FizzBuzzのKataには残り3つのタスクがありますが、この記事ではここで終了します。

まとめ

この記事ではテスト駆動開発を行うにあたって、できるだけ行き詰まらずに進んでいく方法として、小さなステップを優先順位を意識して変更していくことをあげました。
そして、TDD KatasのFizzBuzzで実践してみました。

実践した感触としては、いつもは一気に変更してしまうようなところでもあえて細分化して進めて行くことで、小さなステップで進めていく感覚が掴むことができました。そのおかげか、考え込んで手が止まってしまうような部分が少なかったような気がします。
これを繰り返し実践することで、さらに精度を上げていけそうです。

関連

お題に沿ってテスト駆動開発をやってみた記事です。

  1. 『リファクタリング(第2版)』

  2. The Transformation Priority Premise 2

  3. 『Clean Craftsmanship』 第4章 テスト設計 変換の優先順位説

  4. JavaScriptで[ 0, 1, 2, 3, 4 ]のような連番の配列を生成する方法

22
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
31