お題その4
お題その4:選手の打率を表示用に整形したものを出力できること。(0.333 → .333)
打率が10割の場合は「1.00」と出力すること。
打率を計算しない場合は「----」と出力すること。
このお題では、製品コードに新しいメソッドを追加することになります。その名前は、Get表示用打率()
としましょうか。その中で打率を計算する必要は… ありませんね。お題その3までで作ったCalc打率()
メソッドを呼び出せばよいのですから。
外部設計
これから作るGet表示用打率()
メソッドの外部設計を、今度は先にきっちり考えてみましょう。反応を考えるときに、3つの引数(打席数・打数・安打数)を相手にする必要はなく、Calc打率()メソッドの返値による影響だけを見ればすみます。
作るもの | メソッド「Get表示用打率」 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
置き場所 | クラス「野球選手」 | ||||||||||||||
引数 | int 打席数, int 打数, int 安打数 | ||||||||||||||
返値の型 | string | ||||||||||||||
刺激と反応 |
|
打率が負になる場合と1.0より大きくなる場合はお題に書かれていないため、反応が定義できません。ありえない打率なので、お題に記されていないのでしょう。ここでは、あとで仕様化テストを残しておくことにします。
この反応の一覧をどこかにメモしておきます。なにも上のようにきちんと書く必要はありません。TODOリストとしてテストコードのソースファイルに書いておくのもよいでしょう(実際には、それぞれGREENになったときに、メモを一行ずつ消していきます)。
1つめのテストケース:打率が0以上1.0未満
新しくテストケースのメソッドを書きます。どのケースも難しくなさそうですから、メインとなる処理から始めましょう。
[TestCase(5, 4, 1, ".250")] // 末尾に0が付く public void Get表示用打率Test(int 打席数, int 打数, int 安打数, string 期待値) { 野球選手 p = new 野球選手(); string 打率 = p.Get表示用打率(打席数, 打数, 安打数); Assert.AreEqual(期待値, 打率); }
ここで期待値を".333"
などとしないように気を付けます。有効数字が3桁に満たないとき末尾に0が付くことを確かめるためです。
RED(コンパイルエラー)になることを確認したら、製品コードにGet表示用打率()
メソッドを追加し、このテストケースを満たすだけのコードを書いて、GREENにします。
public string Get表示用打率(int 打席数, int 打数, int 安打数) { decimal? 打率 = this.Calc打率(打席数, 打数, 安打数); return 打率.Value.ToString(".000"); }
この程度は「明白な実装」で楽に書けるでしょう。注意点は、製品コードにこの時点でif
文を書く必要はないということです。テストに通るギリギリ最低限度の製品コードを書くように心掛け、余分なコードを書かないようにします。
2つめのテストケース:打率が1.0
テストケースを追加します。
[TestCase(5, 4, 1, ".250")] [TestCase(5, 4, 4, "1.00")] // ←追加
REDになる("1.000"と一桁多く返ってくる)ことを確認できたら、製品コードを修正します。
public string Get表示用打率(int 打席数, int 打数, int 安打数) { decimal? 打率 = this.Calc打率(打席数, 打数, 安打数); if (打率.Value == 1.0m) // ←追加 return "1.00"; // ←追加 return 打率.Value.ToString(".000"); }
3つめのテストケース:打率がnull
テストケースを追加します。打率がnull
になるのは、打席数が0のときでした。
[TestCase(5, 4, 1, ".250")] [TestCase(5, 4, 4, "1.00")] [TestCase(0, 0, 0, "----")] // ←追加
REDになる(例外が出る)ことが確認できたら、製品コードを変更します。
public string Get表示用打率(int 打席数, int 打数, int 安打数) { decimal? 打率 = this.Calc打率(打席数, 打数, 安打数); if (打率 == null) // ←追加 return "----"; // ←追加 if (打率.Value == 1.0m) return "1.00"; return 打率.Value.ToString(".000"); }
これで、お題その4で求められたことは達成できました。
リファクタリング
製品コードで、打率.Value
を2回書いてあるのが嫌な感じですね。ここをリファクタリングしておきましょう。
public string Get表示用打率(int 打席数, int 打数, int 安打数) { decimal? 打率データ = this.Calc打率(打席数, 打数, 安打数); // ←変数名のリネーム if (打率データ == null) return "----"; decimal 打率 = 打率データ.Value; // ←キャッシュ変数の導入 if (打率 == 1.0m) return "1.00"; return 打率.ToString(".000"); }
ローカル変数のリネームと、値キャッシュ用のローカル変数を導入してみました。オールGREENを確認できたら、リファクタリング完了です。
Get表示用打率()
の中身は、打率計算に1行と、書式化を残りの6行ほどでやっています。1行と6行というのは、アンバランスですね。これもリファクタリングの対象になります。このアンバランスを解消するには、書式化している部分をプライベートメソッドに括り出します。
未定義部分の仕様化テスト
ここではテストファーストからは外れますが、あとでテストコードを見る人(未来の自分を含む)のために、仕様が未定義な部分について、現在の挙動をテストコードとして残しておくことにします(仕様化テスト)。
製品コードがどのような反応をするかを調べるには、適当な期待値を与えてテストを実行し、REDになったときのエラーメッセージを見ます。
[TestCase(5, 4, 1, ".250")] [TestCase(5, 4, 4, "1.00")] [TestCase(0, 0, 0, "----")] [TestCase(5, -4, 1, "????")] // ←追加 [TestCase(5, 4, 45, "????")] // ←追加
TDDBC横浜.野球選手Tests.Get表示用打率Test(5,-4,1,"????"): Expected string length 4 but was 5. Strings differ at index 0. Expected: "????" But was: "-.250" -----------^ TDDBC横浜.野球選手Tests.Get表示用打率Test(5,4,45,"????"): Expected string length 4 but was 6. Strings differ at index 0. Expected: "????" But was: "11.250" -----------^
どういう値が返ってくるか判明したので、そのようにテストケースを直します。
[TestCase(5, 4, 1, ".250")] [TestCase(5, 4, 4, "1.00")] [TestCase(0, 0, 0, "----")] [TestCase(5, -4, 1, "-.250")] //仕様未定義(ありえない引数) [TestCase(5, 4, 45, "11.250")] //仕様未定義(ありえない引数)
オールGREENになることを確かめたら、お題その4は完成です。
事前にTODOリストにしておいたすべてのパターンをクリアできました。自信を持って完成を宣言できますね。
完成したサンプルコード: TddbcYokohamaSample.zip (C# 2010用のソリューション)
GitHubにも置いてあります: https://github.com/biac/tddbc-yokohama20111105/
※ファイル名がShift JISでコミットされてしまっているため、GitHub上では文字化けしていますが、ファイルの中身はちゃんと読めます。
お題その4を振り返る
お題その1~3に比べると、お題その4はあっさりできてしまったようです。作ったコードが簡単だったからでしょうか? 完成した2つのメソッドを見比べてみてください。それほど難易度に違いがあるようには見えないでしょう。
また、Visual Studioの上位版を使ってコードメトリックスを計測してみても、Calc打率()
(お題その1~3)とGet表示用打率()
(お題その4)の保守容易性インデックス(大きいほど理解しやすい)は大して違いがありません(むしろ、お題その4のほうが若干悪い)。お題その4のコードが簡単だったわけではないのです。そうなると、TDDの進め方に違いがあったということになるでしょう。その違いは、お題その1で最初に検討した外部設計が(もちろん恣意的に筆者がそう書いたのですが)いいかげんだったこと、それに起因してテストケースの引数の与え方が不適切になってしまったことなどが挙げられます。事前に外部設計をきちんと検討しておくことで、TDDをうまく進められるのです。
そうは言ってもメソッドの外部設計をうまくできない場合もあります。とりあえず書き進めてみないとどんなメソッドにすればよいのか分からない、ということもあるでしょう。そうではなく、刺激と反応の組み合わせパターンが多すぎて手に負えないということならば、それはメソッドを分割するべきというサインです。