SHOEISHA iD

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

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

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

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

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


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

お題その2

お題その2:打席数が0の場合は、打率を計算しないこと。(Javaならnull、Rubyならnil相当)

 これは、Calc打率()メソッドに変更を加えることになります。引数に渡された打席数が0のときは、nullを返してほしいのですね。今まで返値の型はdecimalとしてきましたが、Null許容型(decimal?)に変えないといけません。

【お題その1+2】の外部設計(太字は変更部分)
作るもの メソッド「Calc打率」
置き場所 クラス「野球選手」
引数 int 打席数, int 打数, int 安打数
返値の型 decimal?
刺激と反応
引数
[打席数]
引数
[打数]
反応
0以外 0以外 返値:打率
0 ゼロ除算例外が発生
0 任意 返値:Null

 さっそくテストケースを書きます。

【テストコード】野球選手Tests.cs(追加部分)
[TestCase(0, 0, 0, null)]
public void Calc打率Test03_打席数0ならnull(int 打席数, int 打数, int 安打数, decimal? 期待値) {
  野球選手 p = new 野球選手();
  Assert.AreEqual(期待値, p.Calc打率(打席数, 打数, 安打数));
}
ちょっと前に会ったような・・・

 おや? この引数の組み合わせは、ゼロ除算例外の仕様化テストと同じですね。このテストにパスするようになると、ゼロ除算例外の方はREDになるでしょう。そのときにまた考えることにします。

 製品コードの修正は簡単です。if文を1つ追加するだけですね。このように、自信があるので三角測量をせず、いきなり製品コードを書き下すことを「明白な実装」(Obvious Implementation)と言います。あ、返値の型も変更しなければなりません。型を変えると、decimalで受けていた既存のテストコードがコンパイルエラーになるので、そこも修正します。

【製品コード】野球選手.cs(部分)
public decimal? Calc打率(int 打席数, int 打数, int 安打数) {  // ←返値の型を変更
  if (打席数 == 0)  // ←追加
    return null;  // ←追加

  return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero);
  //TODO: 四捨五入方式は AwayFromZero で良いか?
}
【テストコード】野球選手Tests.cs(変更部分)
  decimal 打率 = p.Calc打率(打席数, 打数, 安打数).Value;  // ←".Value"を追加

 これで、追加したテストはGREENになりました。しかし、ゼロ除算例外の仕様化テストはREDになってしまいました。打席数に0を渡しているのでnullが返ってくるようになってしまったのですね。テストケースの修正が必要です。

【テストコード】野球選手Tests.cs(部分)
[TestCase(5, 0, 0)]  // ←打席数を0以外の値に変更
public void Calc打率Test02_打数0なら例外(int 打席数, int 打数, int 安打数) {
  野球選手 p = new 野球選手();
  Assert.Throws<System.DivideByZeroException>(() => p.Calc打率(打席数, 打数, 安打数));
}

 これでオールGREENになりました。特にリファクタリングする必要もなさそうなので、お題その2は完了です

お題その3

お題その3:打席数が0でなく、打数が0の場合は、「0.000」と計算すること。

 これも、Calc打率()メソッドに変更を加えることになります。現在、Calc打率()メソッドがどうなっているか、振り返ってみましょう。

 お題その1では、打数が0の場合はゼロ除算例外を発生させました。お題その2では、打席数が0のときは(打数が0であっても)Nullが返ってくるようにしました。併せて考えると、打率が計算されるのは、打席数と打数がともに0以外のときだけとなっています。

【お題その1+2】の刺激と反応(再掲)
引数
[打席数]
引数
[打数]
反応
0以外 0以外 返値:計算した打率(小数点以下3桁)
0 ゼロ除算例外が発生
0 任意 返値:Null

 このお題その3では、いままでゼロ除算例外が発生していたケースで、0.0が返ってくるようになればよいのですね。

【お題その1+2+3】の刺激と反応
引数
[打席数]
引数
[打数]
反応
0以外 0以外 返値:計算した打率(小数点以下3桁)
0 返値:0.0m
0 任意 返値:Null

テストファースト

 それでは、テストケースを修正していきます。

【もとのテストコード】野球選手Tests.cs(部分)
[TestCase(5, 0, 0)]
public void Calc打率Test02_打数0なら例外(int 打席数, int 打数, int 安打数) {
  野球選手 p = new 野球選手();
  Assert.Throws<System.DivideByZeroException>(() => p.Calc打率(打席数, 打数, 安打数));
}
【修正後のテストコード】野球選手Tests.cs(部分)
[TestCase(5, 0, 0)]
public void Calc打率Test02_打数0なら0(int 打席数, int 打数, int 安打数) {  // ←テスト名を変更
  野球選手 p = new 野球選手();
  Assert.AreEqual(0.0m, p.Calc打率(打席数, 打数, 安打数));  // ←変更
}

 予想通りREDになることを確認したら、製品コードを修正します。

【製品コード】野球選手.cs(部分)
public decimal? Calc打率(int 打席数, int 打数, int 安打数) {
  if (打席数 == 0)
    return null;

  if (打数 == 0)  // ←追加
    return 0.0m;  // ←追加

  return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero);
}

 これでオールGREENになりました。

リファクタリング

 重複コードがないか探してみると、テスト側のCalc打率Test01()Calc打率Test02_打数0なら0()を一緒にできそうなことに気づきます。まとめてみましょう。

 まず、Calc打率Test01()に、Calc打率Test02_打数0なら0()と同じテストケースを追加します。

【テストコード】野球選手Tests.cs(部分)
[TestCase(5, 3, 0, 0.0)]
[TestCase(5, 3, 3, 1.0)]
[TestCase(5, 9999, 2345, 0.235)]
[TestCase(5, 0, 0, 0.0)]  // ←追加
public void Calc打率Test01(int 打席数, int 打数, int 安打数, decimal 期待値) {
  野球選手 p = new 野球選手();
  decimal 打率 = p.Calc打率(打席数, 打数, 安打数).Value;
  Assert.AreEqual(期待値, 打率);
}

[TestCase(5, 0, 0)]  // まだ消さない!
public void Calc打率Test02_打数0なら0(int 打席数, int 打数, int 安打数) {
  野球選手 p = new 野球選手();
  Assert.AreEqual(0.0m, p.Calc打率(打席数, 打数, 安打数));
}

 何も壊していないか、全テストを実行して確認します。

 もしここで不安があるようなら、製品コードを一時的に変更して予想通りのREDが出るかどうか確認します。このTestCase(5, 0, 0, 0.0)というのは、製品コードのif (打数 == 0) return 0.0m;の部分に該当する仕様でした。そこで、製品コードを一時的に次のように変えて、テストを実行してみます。

【一時的に変更した製品コード】野球選手.cs(部分)
public decimal? Calc打率(int 打席数, int 打数, int 安打数) {
  if (打席数 == 0)
    return null;

  //if (打数 == 0)  // ← 一時的にコメントアウト
  //  return 0.0m;  // ← 一時的にコメントアウト

  return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero);
}

 予想通り2つのREDが出たでしょうか?

元と移動先の2つのテストケースがREDになる

 安心できたら、製品コードを元に戻して、オールGREENにしておきます。これで安心して、Calc打率Test02_打数0なら0()を削除することができます。

 リファクタリングも完了し、これでお題その3も終了です

打席数0ならnullのケースは?

 Calc打率Test03_打席数0ならnull()もまとめてしまいたいところです。しかし、実はC#の属性にはdecimal型の定数を与えることはできないdouble型として認識され、引数として渡される時にdecimal型へ暗黙の型変換がされている)ため、このままでは1つにできないのです(double型からdecimal?型へは暗黙の型変換が許されていない)。

 引数の期待値の型をdouble?に変えてしまえば可能ですが、そうするとテストケースの意図(Calc打率()の返値はdecimal)があいまいになってしまうので、悩ましいところです。

お題その1~3の外部設計を振り返る

 3つのお題を通してCalc打率()メソッドが完成しました。このメソッドの外部設計がどのようになったのか、まとめておきます。

【お題その1~3】の外部設計
作るもの メソッド「Calc打率」
置き場所 クラス「野球選手」
引数 int 打席数, int 打数, int 安打数
返値の型 decimal?
刺激と反応
引数
[打席数]
引数
[打数]
引数
[安打数]
反応
0 任意 任意 返値:Null
0以外 0 任意 返値:0.0m
0以外 任意 返値:打率(=安打数/打数、小数点以下3桁)

 反応の表を見てください。刺激(3つの引数)の取り得るパターンがすべて網羅されています。

 今回はテストファーストしていく中で右往左往しながらこの表を完成させてきました。逆に、最初にしっかり考えてこの表を完成させていたとしたら、自信を持って、しかも楽にテストファーストを進められたことでしょう。迷いがなければ、RED→GREEN→リファクタリングのリズムにもうまく乗れます。事前に外部設計を考えておくことは、TDDでも大切なのです。

「刺激と反応」

 ちなみにこの刺激と反応の表は、簡易的なデシジョンテーブル(決定表)になっています。なぜ「デシジョンテーブル」ではなく「刺激と反応」という耳慣れない呼び方をしているかというと、引数と返値の他に、状態遷移やデータベースの変化など、ともかくメソッドの動作に影響を及ぼすものすべて/メソッドが影響を与えるものすべてを、そこで検討したいからです。
 

デシジョンテーブル(決定表):正規の表記法は JIS X 0125:1986 「決定表」を参照のこと。

次のページ
お題その4

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

  • 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」など、さまざまなカンファレンスを企画・運営しています。

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

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

メールバックナンバー

アクセスランキング

アクセスランキング