基本: 現在時刻の取得を別にする
制御できないDateTimeOffset.Nowを、とにかく切り離してみましょう。実装を、DateTimeOffset型の引数を受け取るメソッドにすれば、一応は分離することができます。
[TestCase]
public void 時刻をフォーマットするTest_時刻の取得をともかく分離してみる() {
var givenTime = new DateTimeOffset(new DateTime(2012, 8, 8, 14, 3, 0));
var expected = "14時 03分 00秒";
var result = Class1.時刻をフォーマットする(givenTime);
Assert.AreEqual(expected, result);
//製品コードでは Class1.時刻をフォーマットする(DateTimeOffset.Now) のように使う。
}
public static string 時刻をフォーマットする(DateTimeOffset givenTime) {
return givenTime.ToString("hh時 mm分 ss秒"); //注:これはREDになる
}
テストができるようになり、バグも見つけられるようになりました。時刻をフォーマットする()メソッドを呼び出している製品コードを、現在時刻に関係なくテストできるのならばこれで十分です。そうでなければ、呼び出している製品コードのテストも、前と同じ問題を抱えてしまうことになります。
根本的に解決するには、DateTimeOffset.Nowをラップしたメソッドを実装して、完全に分離します。その例をいくつか紹介します。
応用1: 現在時刻取得メソッド + テスト用実装
まず、DateTimeOffset.Nowをラップしたプロパティを実装します。これはハードウェアに密着しているため通常のテストファーストは困難で、手動テストで動作を確認します。
[TestCase]
public void Get現在時刻Test_手動でテスト() {
var nowTime = (new Class2()).現在時刻;
Console.WriteLine(nowTime);
Assert.Inconclusive("コンソール出力を目視確認!");
}
DateTimeOffset.Nowをラップしたプロパティ
public DateTimeOffset 現在時刻{
get { return DateTimeOffset.Now; }
}
この現在時刻プロパティを使って、冒頭のフォーマット済み現在時刻プロパティを書き直すと次のようになります。
フォーマット済み現在時刻プロパティを書き直した
public string フォーマット済み現在時刻 {
get {
return 現在時刻.ToString("hh時 mm分 ss秒");
}
}
このままではテストできないのは、先ほどと変わりません。現在時刻プロパティの中に、テスト用の実装を仕込んでいきます。
まず、製品コード側でプロジェクトのプロパティを開いて、条件付きコンパイルシンボルを定義します。
条件付きコンパイルシンボル: UNIT_TEST
そうしたら、現在時刻プロパティのコードを修正します。
現在時刻プロパティにテスト用実装を追加
#if UNIT_TEST
public DateTimeOffset? テスト用実装_現在時刻 { get; set; }
#endif
public DateTimeOffset 現在時刻{
get {
#if UNIT_TEST
if (テスト用実装_現在時刻.HasValue)
return テスト用実装_現在時刻.Value;
#endif
return DateTimeOffset.Now;
}
}
このテスト用実装は、テストコードからテスト用実装_現在時刻プロパティに値をセットしてやると、現在時刻プロパティはその値を返すようになっています。セットしなければ、本来のコードが実行されます。なお、今回は省略しましたが、テスト用実装も複雑ならばテストファーストで作り込みます。
注意点:
- リリースビルドには入らないようにする
- テストコードからテスト用の値をセットしなかったときは、本来のコードが実行される(従って、手動テストできる)ようにする
- 静的なコードになってしまう場合は、同時実行時の挙動に注意
これで、現在時刻を使うフォーマット済み現在時刻プロパティをテストするコードが書けるようになりました。
[TestCase]
public void Getフォーマット済み現在時刻Test() {
var givenTime = new DateTimeOffset(new DateTime(2012, 8, 8, 14, 3, 0));
var expected = "14時 03分 00秒";
var target = new Class2() { テスト用実装_現在時刻 = givenTime, };
var result = target.フォーマット済み現在時刻;
Assert.AreEqual(expected, result);
}
なお、ここでは説明の都合で順序を前後させました。きちんとTDDを進めていくなら、次の順序になります。
-
Get現在時刻Test_手動でテスト() -
現在時刻プロパティ(テスト実装なし) -
現在時刻プロパティ:テスト用実装のテストコード(今回は省略) -
現在時刻プロパティ:テスト用実装 -
Getフォーマット済み現在時刻Test() -
フォーマット済み現在時刻プロパティ
このようにテスト用の実装を仕込んでしまう方法は、お手軽ではありますが、複雑なものになると対応しきれません。例えば通信ハードウェアに密着した実装を考えてみてください。本来の実装自体が複雑な上に、そこに複雑な応答をシミュレートするテスト用実装を埋め込んだら… きっとスパゲッティコードになってしまうでしょう。本来の実装と、テスト用の実装をきれいに分離する方法が必要になってきます。
