SHOEISHA iD

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

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

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

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

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


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

お題その1

お題その1:入力として、野球選手の打席数と打数と安打数を受け取り、選手の打率を計算できること。 打率は少数第四位で四捨五入すること。
※打率=安打数/打数
※打数とは、打席に立った数のうち、打率の計算に含まない打席を引いたもの。

 さっそくテストケースを書き始めたいところですが…。

 「TDDでは事前設計しない」というのは都市伝説です。 テストケースを書き始めるために必要な設計は、(頭の中だけかもしれませんが)ちゃんと実施します。実際の開発プロジェクトでは、アーキテクチャの検討はもちろんのこと、おおまかなクラスやメッセージの定義などはやっておくべきです。それから、今回のお題のようなメソッドレベルの設計へと進んでいきます。

メソッドの外部設計

 このお題では、どんなもの(what)を作ればよいでしょうか? 打率を計算するメソッドを作ればよいですね。名前は「Calc打率」にしましょう。

 C#ではメソッドだけでは存在できず、その入れ物であるクラスも必要です。どこに入れましょう? TDDでは、迷ったらとりあえずシンプルにしておきます。「野球選手」オブジェクトだけを作って打率を計算することにします。

シンプルじゃないけど良さげなアイデア

 例えば「打撃データ」オブジェクトとして「野球選手」オブジェクトから独立させた方がいいかも、と思ったかもしれません。まずはシンプルに作ることを優先して、そう思ったことは「//TODO:」コメント※6として残しておきましょう。

 他にメソッドを定義するために必要な情報は? 引数返値の型を決めないといけませんね。引数は、整数でよいでしょう。返値は、厳密な小数点数を扱いたいでしょうから、decimal型にしておくべきです。

 また、このメソッドの反応も定義します。このお題ではとてもシンプルで、引数を渡すと打率が返ってくるという反応だけです。…あ、ゼロ除算が起こりえますね、どうしましょう? これもシンプルに考えましょう。つまり、ゼロ除算例外が発生するという反応をするんだと思えばいいんです。

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

 実際には、このような簡単なものなら頭の中だけで済んでしまうでしょう。反応パターン(それぞれがテストケースになり得る)が何通りもあって複雑なときには、「//TODO:」コメントとして先に書いておいた方がよいです。それ以外の項目(メソッド名など)は、テストケースを書くときに考えてもたいてい問題ありません。

他の引数(打席数と安打数)は?

 お題その1からは、他の引数(打席数と安打数)が反応に及ぼす影響が分からないので、上の表には書いてありません。じつは最初に全部(お題その3まで)検討しておいた方がよいのですが、そのことはこの後の展開を読めば分かっていただけるかと思います。

※6 TODOコメント

 Visual Studioでは、メニュー[表示]-[タスク一覧]で表示される「タスク一覧」(そのドロップダウンで「コメント」を選択)に、「//TODO」で始まるコメント行がまとめて表示されます。タスク一覧には、デフォルトでは「//TODO」の他に、「//HACK」「//UNDONE」などがピックアップされます(オプションで変更可能)。

最初のテストケース

 最初のテストケースを書くときには、上述のようにメソッドのシグネチャ(名称、引数、型など)や置き場所など、メソッドの設計にまつわるたくさんの決定をくださなければなりません。せめて、メソッドの応答はもっとも簡単なものから始めましょう。このお題では、

引数:安打数=0 → 応答:返値(打率)=0.0

というケースが、計算しなくても返値が分かって良いでしょう。

テストファースト

 Visual Studioが自動生成した「Class1.cs」を削除して、新しく「野球選手Tests」クラスを作ります。そこにテストケースを次のように記述します。まだ「野球選手」クラスがないのでIDEが警告してきますが、どんどん書いてしまいます。

【テストコード】野球選手Tests.cs
using NUnit.Framework;

namespace TDDBC横浜 {
  [TestFixture()]
  public class 野球選手Tests {
    [TestCase()]
    public void Calc打率Test01_安打数0のとき_打率0() {
      //準備
      int 打席数=0;
      int 打数=0;
      int 安打数 = 0;
      decimal 期待値 = 0.0m;

      野球選手 p = new 野球選手();

      //実行
      decimal 打率 = p.Calc打率(打席数, 打数, 安打数);

      //検証
      Assert.AreEqual(期待値, 打率);

      //後始末
      // (なし)
    }
  }
}

 テストケースの書き方は、手動テストの手順と同じ要領です。テストに必要な準備をして、テスト対象を実際に実行し、その結果を検証します。そのあと必要ならば後始末をします。

 このコードで使っているNUnitの機能は、TestFixtureTestCaseの2つの属性と、Assertクラスです。NUnitは属性を見て、テストとして実行するべきメソッドを自動的に抽出します。Assertクラスには検証するためのメソッドが揃っています。詳細は"NUnit - Documentation"(英文)を参照してください。

 さてこのテストケースは、まだ野球選手クラスを作っていないので、コンパイルするとエラーになります。

コンパイルエラーもRED(テスト失敗)とみなす

 コンパイルエラーもテスト失敗と考えます。テストを失敗させることができた(=RED)ので、テストファーストのルールとして、ようやく製品コードを書けます。そこで、野球選手クラスを作成してコンパイルしてみると、またエラーになります。今度はCalc打率()メソッドがないという失敗ですからメソッドを作ります。メソッドの中身は…? ごくシンプルに0を返すだけのコードを書けば、上のテストケースにパスできるはずです。

【製品コード】野球選手.cs
namespace TDDBC横浜 {
  public class 野球選手 {
    public decimal Calc打率(int 打席数, int 打数, int 安打数) {
      return 0.0m;
    }
  }
}

 このように、コードとして正しくないと分かっているけれど、とりあえずテストを通すためだけのコードを書くやり方を「仮実装」(Fake It)と呼びます。

 コンパイルできたら、いよいよNUnitの出番です。起動したら、メニュー[File]-[Open Project]を使って、コンパイルできたアセンブリファイル「TDDBC横浜.dll」を読み込ませます。[Run]ボタンでテストケースを実行すると…

初めてのGREEN

…見事、GREEN! 最初のテストケースにパスしました。

リファクタリング

 次のテストケースを書く前に、テストコードをリファクタリングしておきましょう。準備のところで引数と期待値を用意していますが、NUnitではTestCase属性を使って次のように書き変えることができます。

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

 何か書き換えたら、NUnitで全部のテストを実行して何も壊していないことを確かめます。さらにテストケース側を変えた場合には、製品コードにわざとバグを入れて、ちゃんとREDになることを確かめると安心できます。ここでは、例えばCalc打率()の返値を1.0mに変えてみてREDになれば、リファクタリング後のテストケースもちゃんと働いていると確信できますね。

わざとREDにして、テストコードがちゃんと働いていることを確かめる

 元に戻してGREENを確かめたら、最初のテストケースは完了です。

打率計算を完成させる

 まだ打率の計算を実装していませんね。今はまだ最初のテストケースにパスするための仮実装、すなわち定数を返しているだけです。もう一つ、例えば打率が10割になるはずのテストケースを追加してみましょう。2つのテストケースから製品コードの同じ箇所を照らし出すこのやり方は、「三角測量」(Triangulate)と呼ばれています。

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

 予定通りREDになりましたでしょうか?

TestCase属性を使ってテストケースを追加した

 これをGREENにするべく、製品コードを修正します。

【製品コード】野球選手.cs(部分)
public decimal Calc打率(int 打席数, int 打数, int 安打数) {
  return 安打数 / 打数;  // ←変更
}

 さぁ、これでオールGREEN …あれっ!? さっきGREENだった最初のテストケースがREDになっています。これはどうしたことでしょう?

さっきまでGREENだったテストケースがREDに!
【テストコード】REDになったテストケース
[TestCase(0, 0, 0, 0.0)]

 テストデータとして打数にも0を渡しています。これが原因でDivideByZeroException(ゼロ除算例外)が発生したのです。しかしこれは、最初に外部設計のところで考えた通りの応答です。つまり間違えているのは、テストケースの方ですから、そちらを直します。

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

 オールGREENを確認したら、テストファーストは完了です。リファクタリングすべきところを、ざっと探します。今回は特になさそうなので、2つめのテストケースも完了です。

不安をテストに

 今回は「偶然に」ゼロ除算例外が発生したので、外部設計で考えていたすべての反応を見ることができました。もしも最初から正しい引数(打数に0ではない数)を与えていた場合は、どうでしょう。ゼロ除算例外が出る反応は見ることができません。また、ゼロ除算例外が出るテストケースをこれから書いても失敗しない(いきなりGREENになる)ので、テストファーストの原則(RED→GREEN)からは外れてしまいます。しかし、本当にゼロ除算例外が出るかどうか不安になったのならば、原則から外れてテストを書けばよいのです。TDDは手段であって目的ではありませんから。

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

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

 繰り返しますが、このテストケースは最初からGREENになるので、テストファーストではありません。不安を解消するために、現状のメソッドの仕様を確認するテストです(「仕様化テスト」(Characterization Test)と呼ばれます)。ゼロ除算例外が出ると分かりきっているのなら書く必要はありません。

見落としはないか

 一通り完成したところで、見落としていることはないか検討します。製品コードをじっくり読んで、足りないコードがないか考えてみましょう。テストケースに誤りや不足はないでしょうか。仕様書(「お題その1」)も読み直してみましょう。

 お題その1を読み返してみると、「打率は少数第四位で四捨五入すること」を忘れていました。テストを追加しましょう。

【テストコード】野球選手Tests.cs(部分)
[TestCase(5, 3, 0, 0.0)]
[TestCase(5, 3, 3, 1.0)]
[TestCase(5, 9999, 2345, 0.235)]  // ←追加 (0.23452345…が四捨五入で0.235になる)
public void Calc打率Test01(int 打席数, int 打数, int 安打数, decimal 期待値) {
  野球選手 p = new 野球選手();
  decimal 打率 = p.Calc打率(打席数, 打数, 安打数);
  Assert.AreEqual(期待値, 打率);
}
ありえない引数の組み合わせ

 「5打席、9999打数」というのはありえない組み合わせです。そのうち打席数と打数の関係についての仕様が明確になって実装したときに、このテストケースは再びREDになるでしょう。そのときに5を10000などに直すも良し、気付いた今のうちに直しておくも良しです。今は、「ありえない組み合わせの引数でも受け付けてしまうメソッドになっているんだ」ということを覚えておきましょう。

 REDを確認したら、これにパスするように製品コードを直します。おっと、四捨五入の方式はどうしましょう? とりあえず5は常に切り上げることにして、後で確認するためTODO:コメントに書いておきましょう。

【製品コード】野球選手.cs(部分)
public decimal Calc打率(int 打席数, int 打数, int 安打数) {
  return Math.Round((decimal)安打数 / 打数, 3, MidpointRounding.AwayFromZero);  // ←変更
  //TODO: 四捨五入方式は AwayFromZero で良いか?
}
宿題です

 この記事を読みながら実際に写経している人は、REDのメッセージを見て、もう一つバグがあったことに気づくと思います。2つめのテストケースでも引数の選び方(5, 3, 3, 1.0)が良くなかったことが原因です。どういう引数にしておけばよかったでしょうか?

 これでもう見落としていることはなさそうです。お題その1は完成です。

次のページ
お題その2

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

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング