« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その2) ~ 最初の仕様変更[T16MAIN-5] | トップページ | [イベント] TDD 道場 ~場外乱闘編~ (2011/10/29) »

2011年8月30日 (火)

[コラム] 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) »

*コラム」カテゴリの記事

<NUnit>」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック


この記事へのトラックバック一覧です: [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その3) ~ 2つめ、3つめの仕様変更[T16MAIN-6,7]:

« [コラム] TDDBC 東京 1.6 のお題を C# でやってみる (その2) ~ 最初の仕様変更[T16MAIN-5] | トップページ | [イベント] TDD 道場 ~場外乱闘編~ (2011/10/29) »