[コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7]
前回は、 [T16MAIN-5] までやりました。 今回のこの2つの仕様変更は、 機能の追加です。 ですから、 新しいメソッドをどんどん TDD していけばよいです。
◆ ひとつめの機能追加
■ T16MAIN-6: dump の引数に時刻を指定できるようにする。 dump 関数は時刻が指定された場合、 指定時刻以降のデータのみを表示する
・dump の引数に時刻(秒単位)を指定できるようにする。
・dump 関数は時刻が指定された場合、 指定時刻以降のデータのみを表示する
Dump() の引数には、 何を渡せば良いでしょう? 「時刻(秒単位)」 という表現は、 .NET Framework 的にはちょっと解釈に困りますね。 DateTime 型にしておきましょう。
※ 後で、 「いや、 現在時刻から遡る時間(秒単位)だ」 ということになれば、 ラッパー関数を書けば終わりますからね。
すると、 オーバーロードする Dump() メソッドのシグネチャは、 次のようになります。
public IList<KeyValueTime> Dump(DateTime time)
テストケースは、 時刻指定付きの Put() を 2回実行してから、 その間の時刻を指定して Dump() すると 1件しか出力されないことを確認すれば、 足りるでしょう。
※ テストメソッドの名前を間違えています。m(_`_)m 「3つ登録して」 ではなくて、 「2つ登録して」 ですね。
[Test()]
public void Dump02Test01_3つ登録してDump_時刻指定でひとつしか出ない() {
const string key1 = "AAA";
const string value1 = "Test1";
DateTime time1 = new DateTime(2011, 8, 3, 17, 40, 0);
const string key2 = "BBB";
const string value2 = "Test2";
DateTime time2 = time1.AddSeconds(1.0); //こっちが新しい → Dump(time2) で、これだけが出て来る
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key1, value1, time1);
dic.Put(key2, value2, time2);
IList<KeyValueTime> dump = dic.Dump(time2);
Assert.That(dump.Count, Is.EqualTo(1));
Assert.That(dump[0].Key, Is.EqualTo(key2));
Assert.That(dump[0].Value, Is.EqualTo(value2));
Assert.That(dump[0].Time, Is.EqualTo(time2));
}
このテストケースに合格するように、 新しい Dump() メソッドを書きます。 保持しているデータの時刻と与えられた時刻を比較して、 条件に合うものだけを取り出せばよいですね。
public IList<KeyValueTime> Dump(DateTime time) {
//return this._dic.Where(kvt => kvt.Time.HasValue ? (kvt.Time.Value >= time) : false).ToList();
return this._dic.Where(kvt => (kvt.Time.Value >= time)).ToList();
}
【注意】 ↑ここで、 コメント行のように、 保持している KeyValuTime が null だったときの場合も書きたくなります。 が、 TDD 三原則からは、 そこはグッと我慢です。
本来なら、 続いて、 時刻が記録されていないデータの扱いをどうするか (上のコメントアウトした実装) を、 考えなければいけません。
…が、 仕様変更 [T16MAIN-8] を先に見てしまったので、 その実装は不要だと分かってしまいました。f(^^; ということで、 省略します。
◆ SystemClock クラスの新設
次の仕様変更 [T16MAIN-7] を見てみると、 現在時刻がどうこうという話が出てきます。 こういうときは、 もはやセオリーとして、 現在時刻を提供してくれるクラスを先行投資で作ってしまいます。
現在時刻というのは、 ハードウェアが提供してくれるデータですから、 それを取得するコードはプログラム外部とのインターフェースになります。 外部とのインターフェース層は独立させるべきですから、 たかが現在時刻を取得するだけでも別のクラスにしておきます。 また、 そうしておくことで、 制御が困難な現在時刻というデータを、 テスト時に制御しやすくなります。
では、 新しく作る SystemClock クラスの仕様ですが、 これはごく簡単なものです。
static な読み出し専用プロパティ Now を持っていて、 これを読みだすとシステムの現在時刻 DateTime.Now が返ってくるだけ。
テストケースと製品コードをいっぺんに示します。
[TestFixture()]
public class SystemClockTest {
[Test()]
public void NowTest01() {
DateTime now = SystemClock.Now;
Assert.That(Math.Abs(DateTime.Now.Subtract(now).TotalMilliseconds), Is.LessThan(1.0));
// ※ 1ミリ秒と違っていなければ OK とする
}
}
internal class SystemClock {
public static DateTime Now {
get {
return DateTime.Now;
}
}
}
◆ SystemClock クラスにテスト用の仕掛けをする
現在時刻が絡んだテストを楽に書くために、 ここで仕掛けを入れておきます。 こんなふうにテストを書きたいわけです。
SystemClock.TestSetTime(new DateTime(2011, 8, 30, 18, 0, 0));
// この行↑以降、 現在時刻を取得すると常に 18:00:00 が返る。
dic.Delete(0, 3); // 18:00:00 から 3秒前、 つまり 17:59:57 以前のデータが消えるはず!
この TestSetTime() メソッドは、 ユニットテストからしか使いません (製品コード内では使わせたくない) ので、 メソッド名をヘンなものとし (頭に "Test" と付けました)、 さらに、 リリースビルドでは無効になるよう、 #if ディレクティブで囲っておきます。
#if DEBUG
[Test()]
public void NowTest02_テスト用のセッター() {
DateTime time = new DateTime(2011, 8, 3, 18, 45, 0); //現実の時刻よりも過去
SystemClock.TestSetTime(time);
Assert.That(SystemClock.Now, Is.EqualTo(time));
// テスト終了時にクリアすること!
SystemClock.TestClearTime();
Assert.That(SystemClock.Now, Is.Not.EqualTo(time));
}
#endif
※ 「テスト終了時にクリアすること!」とコメントしたように、 SystemClock クラスを使うテストでは、 TearDown に TestClearTime() 呼び出しを追加する必要があります。
製品コードは、 次のようになります。 Visual Studio でリリースビルドをするときを想定して、 #if ディレクティブ内の文字色をグレーにしています。
internal class SystemClock {
public static DateTime Now {
get {
#if DEBUG
if (_testTime.HasValue)
return _testTime.Value;
#endif
return DateTime.Now;
}
}
#if DEBUG
static DateTime? _testTime;
internal static void TestSetTime(DateTime time) {
_testTime = time;
}
internal static void TestClearTime() {
_testTime = null;
}
#endif
}
※ 製品コードで、 プロパティにせずにそのままメンバー変数を公開しています。 テスト専用なので、 これでも良いでしょう。
※※ ただし、 コード分析に掛けると、 これは引っ掛かります。 それがマズいなら、 真面目にプロパティにしましょう。 f(^^;
◆ 2つめの機能追加
それでは、 次の機能追加にいきましょう。
■ T16MAIN-7: delete の引数に分・秒を指定できる。 delete は分を指定された場合、「現在時刻-引数の分・秒」よりも古いデータをすべて削除する
・delete の引数に分・秒を指定できる。 delete は分・秒を指定された場合、 データの時刻情報が「現在時刻 - 引数の分・秒」よりも古いデータをすべて削除する
オーバーロードする新メソッドのシグネチャは、 次のようになります。
void Delete(int passedMinute, int passedSecond)
テストケースは、 まず秒のほうから行きましょうか。 (分は 0 に固定)
3つのケースが考えられます。 データの時刻情報が「現在時刻 - 引数の秒」に比べて、「より新しい」、「等しい」、「より古い」 の 3つです。
この 3つのテストケースは、 ひとつのテストメソッドで順番に書けてしまうので、 手抜きしてそうしてしまいましょう。
※ 原則論を言うならば、 ここは 3つのテストメソッドに分けて書くべきところです。
※ 実際に分けてテストケースを書いていくと、 実は 2つのテストケースで完了してしまいます。
なお、 このテストケースは、 SystemClock のテスト用機能を利用するので、 #if DEBUG と、 TearDown() が必要です。
[TearDown()]
public void TearDown() {
#if DEBUG
SystemClock.TestClearTime();
#endif
}
#if DEBUG
[Test()]
public void Delete2Test01_秒を指定() {
const string key1 = "AAA";
const string value1 = "Test1";
DateTime time1 = new DateTime(2011, 8, 3, 17, 40, 0);
const string key2 = "BBB";
const string value2 = "Test2";
DateTime time2 = time1.AddSeconds(1.0);
TddbcDictionary dic = new TddbcDictionary();
dic.Put(key1, value1, time1);
dic.Put(key2, value2, time2);
// 現在時刻を time2 より 2秒後にセット → 現在時刻から見ると、time2 は 2秒前、time1 は 3秒前
SystemClock.TestSetTime(time2.AddSeconds(2.0));
dic.Delete(0, 3);
Assert.That(dic.Dump().Count, Is.EqualTo(2)); //1件も削除されない (より新しい)
dic.Delete(0, 2);
IList<KeyValueTime> dump = dic.Dump();
Assert.That(dump.Count, Is.EqualTo(1)); //time1 だけ削除される (より古い と 等しい)
Assert.That(dump[0].Key, Is.EqualTo(key2)); //time2 の方のデータは残っている
dic.Delete(0, 1);
Assert.That(dic.Dump().Count, Is.EqualTo(0)); //time2 も削除される (より古い … 実は不要)
}
#endif
製品コードは、 現在時刻から指定された秒数だけ過去の時刻を算出し、 それよりも古いデータを削除するだけです。
internal void Delete(int passedMinute, int passedSecond) {
DateTime limitTime = SystemClock.Now.AddSeconds(-passedSecond);
this._dic.RemoveWhere(kvt => (kvt.Time < limitTime));
}
分もまったく同様にして出来ますね。 製品コードだけ示します。
※ 手抜きして、 秒と同様のテストケースを書きましたが、 実はこちらはテストケースひとつで完了します。
internal void Delete(int passedMinute, int passedSecond) {
DateTime limitTime = SystemClock.Now.AddMinutes(-passedMinute).AddSeconds(-passedSecond);
this._dic.RemoveWhere(kvt => (kvt.Time < limitTime));
}
この時点でのソース:
テストケース https://github.com/biac/tddbc-tokyo-1.6/tree/fbd7dda9a4eb3eae53bd6e56bce3053966ac6a22/TddbcTokyo16Test
製品コード https://github.com/biac/tddbc-tokyo-1.6/tree/fbd7dda9a4eb3eae53bd6e56bce3053966ac6a22/TddbcTokyo16
あ… 追加した Delete() メソッドが、 スケルトンを自動生成させたときの internal のままですね。 バグってます (汗;
あとできっと直すことでしょう。 f(^^;
さて、 もうちっとだけ仕様変更が続くのです… (続く)
| 固定リンク
« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その2) ~ 最初の仕様変更[T16MAIN-5] | トップページ | [イベント] TDD 道場 ~場外乱闘編~ (2011/10/29) »
「*コラム」カテゴリの記事
- [コラム] Visual Studio 11 に統合できるテスティング フレームワーク(2012.03.22)
- [コラム] TDD のパターン: Assert First(2012.02.09)
- [TDD Advent Calendar jp: 2011] TDD とアジャイルを支えるバックボーン #TddAdventJp(2011.12.25)
- [コラム] TDD の原点 ~ Kent Beck による定義(2011.12.13)
- [コラム] TDD は止めて、 DbE (例示による設計) と呼ぼう!(2011.11.03)
「<NUnit>」カテゴリの記事
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発 第2回/第3回(2012.04.13)
- [記事紹介] CodeZine ~ C#で始めるテスト駆動開発(2011.12.12)
- [コラム] Visual Studio 11 に統合できるテスティング フレームワーク(2012.03.22)
- [NEWS] NUnit 2.6.0 RC リリース(2012.02.09)
- [NEWS] NUnit 2.6 正式リリース(2012.02.28)

コメント