SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

C#で始めるテスト駆動開発入門

C#で始めるテスト駆動開発
~TDDBC横浜の課題をやってみよう

C#で始めるテスト駆動開発入門(1)


  • X ポスト
  • このエントリーをはてなブックマークに追加

お題その4

お題その4:選手の打率を表示用に整形したものを出力できること。(0.333 → .333)
    打率が10割の場合は「1.00」と出力すること。
    打率を計算しない場合は「----」と出力すること。

 このお題では、製品コードに新しいメソッドを追加することになります。その名前は、Get表示用打率()としましょうか。その中で打率を計算する必要は… ありませんね。お題その3までで作ったCalc打率()メソッドを呼び出せばよいのですから。

これから作るメソッドはCalc打率()を呼び出す
Get表示用打率()はCalc打率()を呼び出す

外部設計

 これから作るGet表示用打率()メソッドの外部設計を、今度は先にきっちり考えてみましょう。反応を考えるときに、3つの引数(打席数・打数・安打数)を相手にする必要はなく、Calc打率()メソッドの返値による影響だけを見ればすみます。

【お題その4】の外部設計
作るもの メソッド「Get表示用打率」
置き場所 クラス「野球選手」
引数 int 打席数, int 打数, int 安打数
返値の型 string
刺激と反応
他メソッドの返値 反応
Calc打率() 返値
null "----"
負数 (未定義)
0以上 1.0未満 ".000"('0'は数字。小数点以下は常に3桁)
1.0 "1.00"
1.0より大きい (未定義)
「(未定義)」の反応

 打率が負になる場合と1.0より大きくなる場合はお題に書かれていないため、反応が定義できません。ありえない打率なので、お題に記されていないのでしょう。ここでは、あとで仕様化テストを残しておくことにします。

お題その4のTODOリスト(最初)

 この反応の一覧をどこかにメモしておきます。なにも上のようにきちんと書く必要はありません。TODOリストとしてテストコードのソースファイルに書いておくのもよいでしょう(実際には、それぞれGREENになったときに、メモを一行ずつ消していきます)。

1つめのテストケース:打率が0以上1.0未満

 新しくテストケースのメソッドを書きます。どのケースも難しくなさそうですから、メインとなる処理から始めましょう。

【テストコード】野球選手Tests.cs(追加部分)
[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にします。

【製品コード】野球選手.cs(追加部分)
public string Get表示用打率(int 打席数, int 打数, int 安打数) {
  decimal? 打率 = this.Calc打率(打席数, 打数, 安打数);
  return 打率.Value.ToString(".000");
}
お題その4のTODOリスト(1つ完了)

 この程度は「明白な実装」で楽に書けるでしょう。注意点は、製品コードにこの時点でif文を書く必要はないということです。テストに通るギリギリ最低限度の製品コードを書くように心掛け、余分なコードを書かないようにします。

2つめのテストケース:打率が1.0

 テストケースを追加します。

【テストコード】野球選手Tests.cs(部分)
[TestCase(5, 4, 1, ".250")]
[TestCase(5, 4, 4, "1.00")]  // ←追加

 REDになる("1.000"と一桁多く返ってくる)ことを確認できたら、製品コードを修正します。

【製品コード】野球選手.cs(部分)
public string Get表示用打率(int 打席数, int 打数, int 安打数) {
  decimal? 打率 = this.Calc打率(打席数, 打数, 安打数);
  
  if (打率.Value == 1.0m)  // ←追加
    return "1.00";  // ←追加
  
  return 打率.Value.ToString(".000");
}
お題その4のTODOリスト(2つ完了)

3つめのテストケース:打率がnull

 テストケースを追加します。打率がnullになるのは、打席数が0のときでした。

【テストコード】野球選手Tests.cs(部分)
[TestCase(5, 4, 1, ".250")]
[TestCase(5, 4, 4, "1.00")]
[TestCase(0, 0, 0, "----")]  // ←追加

 REDになる(例外が出る)ことが確認できたら、製品コードを変更します。

【製品コード】野球選手.cs(部分)
public string Get表示用打率(int 打席数, int 打数, int 安打数) {
  decimal? 打率 = this.Calc打率(打席数, 打数, 安打数);

  if (打率 == null)  // ←追加
    return "----";  // ←追加

  if (打率.Value == 1.0m)
    return "1.00";

  return 打率.Value.ToString(".000");
}
お題その4のTODOリスト(3つ完了)

 これで、お題その4で求められたことは達成できました。

リファクタリング

 製品コードで、打率.Valueを2回書いてあるのが嫌な感じですね。ここをリファクタリングしておきましょう。

【製品コード】野球選手.cs(部分)
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になったときのエラーメッセージを見ます。

【テストコード】野球選手Tests.cs(部分)
[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"
  -----------^

 どういう値が返ってくるか判明したので、そのようにテストケースを直します。

【テストコード】野球選手Tests.cs(部分)
[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")]  //仕様未定義(ありえない引数)
お題その4のTODOリスト(すべて完了)

 オールGREENになることを確かめたら、お題その4は完成です

 事前にTODOリストにしておいたすべてのパターンをクリアできました。自信を持って完成を宣言できますね。

ソースコード

完成したサンプルコード: TddbcYokohamaSample.zip (C# 2010用のソリューション)
GitHubにも置いてあります: https://github.com/biac/tddbc-yokohama20111105/


※ファイル名がShift JISでコミットされてしまっているため、GitHub上では文字化けしていますが、ファイルの中身はちゃんと読めます。

お題その4を振り返る

 お題その1~3に比べると、お題その4はあっさりできてしまったようです。作ったコードが簡単だったからでしょうか? 完成した2つのメソッドを見比べてみてください。それほど難易度に違いがあるようには見えないでしょう。

Visual Studio のコードメトリックス

 また、Visual Studioの上位版を使ってコードメトリックスを計測してみても、Calc打率()(お題その1~3)とGet表示用打率()(お題その4)の保守容易性インデックス(大きいほど理解しやすい)は大して違いがありません(むしろ、お題その4のほうが若干悪い)。お題その4のコードが簡単だったわけではないのです。そうなると、TDDの進め方に違いがあったということになるでしょう。その違いは、お題その1で最初に検討した外部設計が(もちろん恣意的に筆者がそう書いたのですが)いいかげんだったこと、それに起因してテストケースの引数の与え方が不適切になってしまったことなどが挙げられます。事前に外部設計をきちんと検討しておくことで、TDDをうまく進められるのです。

 そうは言ってもメソッドの外部設計をうまくできない場合もあります。とりあえず書き進めてみないとどんなメソッドにすればよいのか分からない、ということもあるでしょう。そうではなく、刺激と反応の組み合わせパターンが多すぎて手に負えないということならば、それはメソッドを分割するべきというサインです。

次のページ
まとめ

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
C#で始めるテスト駆動開発入門連載記事一覧

もっと読む

この記事の著者

biac(ばいあっく)

HONDA R&Dで自動車の設計をやっていた機械屋さんが、技術の進化スピードに魅かれてプログラマーに。以来30年ほど、より良いコードをどうやったら作れるか、模索の人生。わんくま同盟の勉強会(名古屋)で、よく喋ってたりする。2014/10~2019/6 Microsoft MVP (Windows Devel...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/6300 2012/07/09 15:35

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング