2012年8月7日火曜日

FizzBuzz をテストファーストで書いてみよう

なんとなく FizzBuzz をテストファーストで書いてみることにした。

====

Eclipse 上で JMockit を使ってやってみる。

仕様は、3の倍数なら Fizz、5の倍数なら Buzz、3と5の公倍数なら FizzBuzz、それ以外ならその整数自体を標準出力に書き出すというもの。範囲は 1 から 100 までにしよう。main() から実行する。

まずテストクラスから

public class FizzBuzzTest {
  @Test public void main() {
    FizzBuzz.main(new String[0]);
  }
}
クラス FizzBuzz が存在しないので、エディタ上で赤い下線が引かれている。Ctrl+1 でクラス生成ダイアログを開いて、main() メソッド付きのクラスを自動作成する。
public class FizzBuzz {
  /**
   * @param args
   */
  public static void main(String[] args) {
    // TODO Auto-generated method stub
  }
}
コンパイルが通ったら一応 JUnit を実行してグリーンになるのを確認。


次に、とりあえず一番最初に"1" が表示されることを確認したい。テストコードの方を以下のように書き換える。

public class FizzBuzzTest {
  @Mocked("println(String)") PrintStream mock;
  @Test public void main() {
    new Expectations() {{
      mock.println("1");
    }};
    FizzBuzz.main(new String[0]);
  }
}
PrintStream をモックして、System.out.println()の呼び出しを確かめている。

これを実行すると、パラメータ"1"で呼ばれるはずの println() が呼ばれなかったという事で、になる。本体コードを以下のように直して、再実行。今度はグリーンになるので、JMockit を含めた疎通確認ができた。
  public static void main(String[] args) {
    System.out.println("1");
  }


さて、与えられた仕様をどうテストするか。まず 100個の数字ってとこからやってみる。実装としては、println() が呼ばれる度に、パラメータを List に溜め込んでおいて、後で調べる方式にしてみよう。こんなコードになる。

  @Test public void main() {
    final List<String> texts = new ArrayList<>();
    new NonStrictExpectations() {{
      mock.println(anyString); result = new Delegate<PrintStream>() {
        void println(String x) { texts.add(x); }
      }; 
    }};
    FizzBuzz.main(new String[0]);
    assertEquals(100, texts.size());
}
Delegate ってのを使って、PrintStream#println(String) を書き換えて、受け取った文字列をローカルのリスト texts に溜め込むようにした。

実行すると赤くなるから、本体コードも以下のように直して、グリーンにしておく。
  public static void main(String[] args) {
    for (int i = 1; i <= 100; i++)
      System.out.println("" + i);
  }


次に、数字と文字列の対応付けのあたりをやる。以下をチェックする。

始点の1・・・1 ⇨ "1"
3とその前後・・・2 ⇨ "2", 3 ⇨ "Fizz", 4 ⇨ "4"
5とその前後・・・4 ⇨ "4", 5 ⇨ "Buzz", 6 ⇨ "Fizz"
15とその前後・・・14 ⇨ "14", 15 ⇨ "FizzBuzz", 4 ⇨ "16"
終点の100・・・100 ⇨ "Buzz"
テストコードはこんな感じにしてみる。
@SuppressWarnings("unused")
public class FizzBuzzTest {
  @Mocked("println(String)") PrintStream mock;
  @Test public void main() {
    final List<String> texts = new ArrayList<>();
    new NonStrictExpectations() {{
      mock.println(anyString); result = new Delegate<PrintStream>() {
        void println(String x) { texts.add(x); }
      }; 
    }};
    FizzBuzz.main(new String[0]);
    assertEquals(100, texts.size());

    FizzBuzzAssertion a = new FizzBuzzAssertion(texts);
    a.assertIt("1",      1);

    a.assertIt("2",      2);
    a.assertIt("Fizz",   3);
    a.assertIt("4",      4);
    a.assertIt("Buzz",   5);
    a.assertIt("Fizz",   6);
  
    a.assertIt("14",     14);
    a.assertIt("FizzBuzz",15);
    a.assertIt("16",     16);

    a.assertIt("Buzz",   100);
  }
  static class FizzBuzzAssertion {
    final List<String> actuals;
    FizzBuzzAssertion(List<String> actuals) { this.actuals = actuals; }
    void assertIt(String expected, int number) {
      assertEquals(expected, actuals.get(number - 1));
    }
  }
}
FizzBuzzAssertion クラスはコード重複を避けるために導入した。そうしないと、以下のようなコード重複が生じる。また、リストの数字と文字列の対応付けもずれて読みにくいので、これも解消した。
assertEquals("1", texts.get(0));
assertEquals("2", texts.get(1));
assertEquals("Fizz", texts.get(2));
assertEquals("4", texts.get(3));
テストを実行するとになるので、本体コードを以下のように書き換える。
  public static void main(String[] args) {
    for (int i = 1; i <= 100; i++) {
      System.out.println(
          0 == i % 15 ? "FizzBuzz":
          0 == i % 3 ?  "Fizz":
          0 == i % 5 ?  "Buzz":
          Integer.toString(i));
    }
  }
再実行してグリーンになるのを確認。これでテストコードと本体コードの TDD 終わり。

一応機能テストとして、本体コードの main を実行して、コンソールを目視確認しておく。問題なし。

====

こんな感じで、FizzBuzz 自体は屁だけど、テストファーストでやるとモッキングのテクが、若干必要になってくる。

0 件のコメント:

コメントを投稿