« [わんくま同盟 東京勉強会] 第27回 (2008/12/20) の資料 | トップページ | [.NET] SandCastle 用に、 namespace へのコメントをソースコードに埋め込む方法 »

2008年12月21日 (日)

[TDD の練習] FizzBuzz 問題

わんくま同盟 東京勉強会 #27上野さんセッションで紹介された、 FizzBuzz 問題。
100 まで表示せい、 ってお題に、 即座にその場で書き下した yukiyukki初音さん、 さすがです。

けど、 あれれ? 0 始まりじゃなくて、 1 からだよ~ f(^^;
あ、 それだけじゃないよ~な…

これって意外と難しい? f(^^;

ということで、 NUnit ( VS 2008 Pro. 以上を持ってるなら、 そのユニットテストでも OK ) 用のテストコードを考えてみてください。

って、 いきなり言われても困る? f(^^;

最初は、 仕様の確認から。
まず、 FizzBuzz 問題ってのは、 Wikipedia から引用すると、 こんなんです。

最初のプレイヤーは 「1」 と数字を発言する。 次のプレイヤーは直前のプレイヤーの次の数字を発言していく。 ただし、 3で割り切れる場合は  「Fizz」 (Bizz Buzz の場合は 「Bizz」)、 5で割り切れる場合は 「Buzz」、 両者で割り切れる場合は 「Fizz Buzz」 (Bizz Buzz の場合は 「Bizz Buzz」) を数の代わりに発言しなければならない。

ゲームは、 以下のとおりに発言が進行する。
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16,

これに加えて、 上野さんのお題では、 100 まで、 という条件が追加されました。 ( たしか f(^^; )

では、 どんなテストをすれば上記の仕様を満たせているかどうかを検証できるでしょうか?
プログラムの最終出力は、
  1: 1
  2: 2
  3: Fizz
  4: 4
  5: Buzz

100: Buzz
と、 100行出力されます。
この出力をまとめて取り込んで、 n=1 から 100 までの全ての結果が正しいかどうか検証しますか?

100個の値を検証するのは、 やればやれるので、 そうやってもかまいませんし、 プログラムに求められる信頼性によっては、 そこまでやらないといけない場合もあるでしょう。

しかし、 100 までじゃなくて、 1000 までだったら? 1億までだったら?
抜き取り検査にして、 大部分の検証を省略しないとやってられませんね。
1億個の検証コードを書く代わりに、 こことここを押さえておけばあとは大丈夫 ( …なはず ) というところを選び出してテストコードを書きます。

どう選び出すのか、 そこがテストケースを作るポイントになります。
さらに、 TDD の場合には、 開発しやすくするためのテストケースや、 アーキテクチャへの 「口出し」 もあります。

※ アーキテクチャへの 「口出し」 は、 たとえばこんなことです。
この FizzBuzz では、 100行の出力をキャプチャして、 あとから 100行を順に見ていけば、 検証できます。 けど、 それではテストコードを書くのが面倒なので…
1. 数値を与えて、 数字か Fizz/Buzz かを答えさせるメソッドを独立させる
2. メインメソッドからは、 そのメソッドをよびだす
…というカタチにしちゃいます。

※※ そうすると、 メインメソッドはこんなふうになります。
class Program {
  static void Main(string[] args) {
    Console.WriteLine("Say FizzBuzz, from 1 to 100 !");
    for (int i = 1; i <= 100; i++) {
      Console.WriteLine(FizzBuzz.SayFizzBuzz(i));
    }
    //Console.ReadKey();
  }
}

これはテスト省略というか、 動作確認してアタマとシッポが合ってれば OK! くらいのシンプルなコードですね。

では、 どんなテストコードを書いていけばいいのか、 文章で書いてみます。

  1. n=1 のとき。
    クラスやメソッドのカタチを決めて、 とりあえずコンパイルできてユニットテストを流せるようにするために、 もっとも単純なテストケースを、 最初に作ります。
    ※ 実装は、 return "1"; だけで OK !

  2. n=2 のとき
    このテストケースを追加してあげると、 ロジックの基本 ( 普段は数字を返す )  が出来ているかどうかテストできます。

  3. n=3 と 5 のとき
    3 と 5 は、 数字じゃなくて Fizz/Buzz で答えなきゃいけないケースですね。
    n=4 は飛ばしてしまって… や、 たいしたことないから、 そこもテストを書いておきますか。 f(^^;

  4.   n が 3 の倍数のとき
    このときも Fizz と答えなければなりません。 n=9 のケースを追加すればよさそうです。

  5. n が 5 の倍数のとき
    このときも Buzz と答えなければなりません。 n=10 のケースを追加すればよさそうです。

  6. n が 3 と 5 の公倍数のとき
    このときは "Fizz Buzz" と答えなければなりません。 n=15 のケースを追加すればよさそうです。

ここまでのテストで、 1 ~ 100 までの値に対して Fizz/Buzz を答えるメソッドが実装できるはずです。
これでテストは十分かな…?

境界値前後はバグが潜む絶好の場所ですから、 ちゃんと検証しておきましょう。
以降で追加するテストケースは、 (ここまでの実装が正しければ) 基本的にはそのまま OK になるはずです。

数字から Fizz に変わるところと、 Fizz から数字に変わるところは、n=2, 3 とn=3, 4 で、 これは 2. と 3. で実施しました。
数字から Buzz に変わるところは n=4, 5 で、 これもやってあります。

  1. Buzz から数字に変わるところは… n=10, 11 ですね。 n=11 のケースが無いので、 追加します。

  2. 数字から Fizz Buzz に変わるところと、 また数字に変わるところは、 n=14, 15, 16 です。 n=14 と n=16 のケースを追加します。

…途中の境界値は、 こんなもんかな。

両端の境界値は、 どうでしょう?

  1. 小さいほうは… 仕様では 1 から始まるので、 0 や負の値を渡したときは、 結果は得られなくても構いませんね。 ArgumentOutOfRangeException を投げてもらうことにして、 そのテストケースを書きます。
    ※ 現実の開発では、 仕様決定者に確認を取る必要があるかもしれません。
    ※ このテストは失敗します。 ArgumentOutOfRangeException を投げる実装を追加することになります。

  2. 大きいほうは、 int.MaxValue を与えて答えが返ってくるかのテストでよいでしょう。
    ※ 仕様の 「100 まで」 の解釈によっては、 101 以降は例外を投げるべきかもしれません。
    ※ int.MaxValue ではオーバーフローする場合、 オーバーフローするというテストと、 オーバーフローしない限界値のテストケースを追加します。

回答例 ( C# + NUnit 用のソースコード ) はこちら → FizzBuzzTest.cs [2KB]
ちゃんと自分で書いてみてから、 見てね f(^^;

そして、 このテストコードをパスするように実装とリファクタリングしたコードは、 こうなりました。

using System;
using System.Globalization;

namespace FizzBuzzQuestion {
  public class FizzBuzz {
    public static string SayFizzBuzz(int n) {
      if (n <= 0)
        throw new ArgumentOutOfRangeException("n", "n は 1以上の整数でなければなりません。");

      bool isMultipleOf3 = (n % 3 == 0);
      bool isMultipleOf5 = (n % 5 == 0);

      if (isMultipleOf3 && isMultipleOf5)
        return "Fizz Buzz";

      if (isMultipleOf3)
        return "Fizz";

      if (isMultipleOf5)
        return "Buzz";

      return n.ToString(CultureInfo.InvariantCulture);
    }
  }
}

最後に、 回答例のテストケースの同値分割表を載せておきます。
※ 「同値分割」 にリンクを付けておこうと思ったら… そこら中、 間違えてやがんの orz
「有効値の集合と無効値の集合に分け」 って… つねに valid と invalid の 2種類しかパーティションが無いのかよ~ (--;

パーティションテストした n期待値
0 または 負の整数 0, -1 例外が出る
正の整数 3 の倍数である 5 の倍数である 15 "Fizz Buzz"
5 の倍数ではない 3, 9 "Fizz"
3 の倍数ではない 5 の倍数である 5, 10 "Buzz"
5 の倍数ではない 1, 2, 4, 11, 14, 16, int.MaxValue 数字


( 2008/12/23 追記 )
私は、 普段はビジネスアプリを書いてるんですが、 このコードならば、 上記のテストケースのうち、 途中の境界条件 ( 7. と 8. ) は書かないでしょうね。
実装したコードでは、 ( n - 1 ) のときの値や ( n + 1 ) のときの値などは一切使っていないことが明白ですから、 つまり前後の値に影響されないって分かりますから、 省略しても大丈夫と判断します。
※ 将来、 コードが書き換えられて、 前後に影響されるようになってしまうことが無いとは言えないので、 求められる品質によっては省略できないこともあるでしょう。

|

« [わんくま同盟 東京勉強会] 第27回 (2008/12/20) の資料 | トップページ | [.NET] SandCastle 用に、 namespace へのコメントをソースコードに埋め込む方法 »

プログラミング」カテゴリの記事

-プログラミング ( わんくま )」カテゴリの記事

コメント

biacさんのセッション面白かったピヨ♪
大変為になりました。
この問題解けない人が居るっていうのは意外だったピヨ。
ボクはてっきり、上野さんのことだからC言語でバッファオーバーフローなどの対策を施す難問かと思ったピヨ。
クラック出来ないコードは凄く難しいピヨ。
そらならば、ボクも自信ないピヨ。
ボクは直ぐ緊張するから、2分で出来るかどうかはわからないけど、でも解けないなんて考えられないピヨ。
それで思ったんだけど、面接試験ではテストコードと実装コードの二つを提出させたらいいと思うピヨ。

投稿: インドリ | 2008年12月21日 (日) 21時52分

> biacさんのセッション面白かったピヨ♪

ありがとうございます (^^;

> この問題解けない人が居るっていうのは意外だったピヨ。

FizzBuzz の話は、 1年半くらい昔に読んだことがあるので…

で、 この FizzBuzz の回答は理解できませんでしたw
http://slashdot.jp/it/comments.pl?sid=362696&cid=1162670

# /. のレスにあるように、 もちろんネタね f(^^;

> ボクは直ぐ緊張するから、2分で出来るかどうかはわからないけど、でも解けないなんて考えられないピヨ。

入社試験の面接で、 いきなりコレを言われて 2分では… 私は落ちるw

私は最初の就職のとき、 二次面接で、 飛行機の翼の曲げモーメント分布を、 っていきなりホワイトボードに書かされて説明させられて… ちゃんと答えられなかったって前科もあるし~ f(^^;
# 航空工学科っていったって、 自動制御をやってたんだいっ! f(^^;;;

> それで思ったんだけど、面接試験ではテストコードと実装コードの二つを提出させたらいいと思うピヨ。

ぉお! いいかも (^^)

投稿: biac | 2008年12月21日 (日) 23時23分

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/43484681

この記事へのトラックバック一覧です: [TDD の練習] FizzBuzz 問題:

» [TDD の練習(2)] (すっかり出遅れた) 縦書き祭 [biac の それさえもおそらくは幸せな日々@nifty]
わんくま blog での縦書き祭は → このへんから てことで、 仕様はこんな感じ。・ 入力は全角のテキストだとする。 ( そうじゃなかったら、 読み込んだときに変換してね )・ Console.WriteLine() で出力していくと、 縦書きしてあるように見えること。・ 無駄な空白 (右端と下端) は、 出力しない... [続きを読む]

受信: 2008年12月30日 (火) 03時42分

« [わんくま同盟 東京勉強会] 第27回 (2008/12/20) の資料 | トップページ | [.NET] SandCastle 用に、 namespace へのコメントをソースコードに埋め込む方法 »