お題その2
お題その2:打席数が0の場合は、打率を計算しないこと。(Javaならnull、Rubyならnil相当)
これは、Calc打率()
メソッドに変更を加えることになります。引数に渡された打席数が0のときは、null
を返してほしいのですね。今まで返値の型はdecimal
としてきましたが、Null許容型(decimal?
)に変えないといけません。
作るもの | メソッド「Calc打率」 | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
置き場所 | クラス「野球選手」 | |||||||||||
引数 | int 打席数, int 打数, int 安打数 | |||||||||||
返値の型 | decimal? | |||||||||||
刺激と反応 |
|
さっそくテストケースを書きます。
[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で受けていた既存のテストコードがコンパイルエラーになるので、そこも修正します。
public decimal? Calc打率(int 打席数, int 打数, int 安打数) { // ←返値の型を変更 if (打席数 == 0) // ←追加 return null; // ←追加 return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero); //TODO: 四捨五入方式は AwayFromZero で良いか? }
decimal 打率 = p.Calc打率(打席数, 打数, 安打数).Value; // ←".Value"を追加
これで、追加したテストはGREENになりました。しかし、ゼロ除算例外の仕様化テストはREDになってしまいました。打席数に0を渡しているのでnull
が返ってくるようになってしまったのですね。テストケースの修正が必要です。
[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以外のときだけとなっています。
引数 [打席数] |
引数 [打数] |
反応 |
---|---|---|
0以外 | 0以外 | 返値:計算した打率(小数点以下3桁) |
0 | ゼロ除算例外が発生 | |
0 | 任意 | 返値:Null |
このお題その3では、いままでゼロ除算例外が発生していたケースで、0.0が返ってくるようになればよいのですね。
引数 [打席数] |
引数 [打数] |
反応 |
---|---|---|
0以外 | 0以外 | 返値:計算した打率(小数点以下3桁) |
0 | 返値:0.0m | |
0 | 任意 | 返値:Null |
テストファースト
それでは、テストケースを修正していきます。
[TestCase(5, 0, 0)] public void Calc打率Test02_打数0なら例外(int 打席数, int 打数, int 安打数) { 野球選手 p = new 野球選手(); Assert.Throws<System.DivideByZeroException>(() => p.Calc打率(打席数, 打数, 安打数)); }
[TestCase(5, 0, 0)] public void Calc打率Test02_打数0なら0(int 打席数, int 打数, int 安打数) { // ←テスト名を変更 野球選手 p = new 野球選手(); Assert.AreEqual(0.0m, p.Calc打率(打席数, 打数, 安打数)); // ←変更 }
予想通りREDになることを確認したら、製品コードを修正します。
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()
と同じテストケースを追加します。
[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;
の部分に該当する仕様でした。そこで、製品コードを一時的に次のように変えて、テストを実行してみます。
public decimal? Calc打率(int 打席数, int 打数, int 安打数) { if (打席数 == 0) return null; //if (打数 == 0) // ← 一時的にコメントアウト // return 0.0m; // ← 一時的にコメントアウト return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero); }
予想通り2つのREDが出たでしょうか?
安心できたら、製品コードを元に戻して、オールGREENにしておきます。これで安心して、Calc打率Test02_打数0なら0()
を削除することができます。
リファクタリングも完了し、これでお題その3も終了です。
Calc打率Test03_打席数0ならnull()
もまとめてしまいたいところです。しかし、実はC#の属性にはdecimal型の定数を与えることはできない(double
型として認識され、引数として渡される時にdecimal
型へ暗黙の型変換がされている)ため、このままでは1つにできないのです(double
型からdecimal?
型へは暗黙の型変換が許されていない)。
引数の期待値の型をdouble?
に変えてしまえば可能ですが、そうするとテストケースの意図(Calc打率()
の返値はdecimal
)があいまいになってしまうので、悩ましいところです。
お題その1~3の外部設計を振り返る
3つのお題を通してCalc打率()
メソッドが完成しました。このメソッドの外部設計がどのようになったのか、まとめておきます。
作るもの | メソッド「Calc打率」 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
置き場所 | クラス「野球選手」 | |||||||||||||||
引数 | int 打席数, int 打数, int 安打数 | |||||||||||||||
返値の型 | decimal? | |||||||||||||||
刺激と反応 |
|
反応の表を見てください。刺激(3つの引数)の取り得るパターンがすべて網羅されています。
今回はテストファーストしていく中で右往左往しながらこの表を完成させてきました。逆に、最初にしっかり考えてこの表を完成させていたとしたら、自信を持って、しかも楽にテストファーストを進められたことでしょう。迷いがなければ、RED→GREEN→リファクタリングのリズムにもうまく乗れます。事前に外部設計を考えておくことは、TDDでも大切なのです。
ちなみにこの刺激と反応の表は、簡易的なデシジョンテーブル(決定表)になっています。なぜ「デシジョンテーブル」ではなく「刺激と反応」という耳慣れない呼び方をしているかというと、引数と返値の他に、状態遷移やデータベースの変化など、ともかくメソッドの動作に影響を及ぼすものすべて/メソッドが影響を与えるものすべてを、そこで検討したいからです。
※ デシジョンテーブル(決定表):正規の表記法は JIS X 0125:1986 「決定表」を参照のこと。