« [イベント] TDD Boot Camp、いっぺんに 30組以上がペアプログラミングする壮観! | トップページ | [イベント] 告知 - Tech Fielders セミナー 東京 [Agile Day 「A Start - アジャイル開発が気になるあなたに贈るセッション&ワークショップ] ( 1/22, 新宿 ) »

2009年12月22日 (火)

[コラム] どうやって DateTime.Now を含むメソッドをテストする?

TDD Boot Camp でも、 演習の後半に出てきました。 テスト対象のメソッドが、 現在時刻や時間経過に依存しているときは、 どうやったら上手くユニット テストが書けるでしょう?
テストしたい時刻になるまで、 あるいは、 テストしたい時間が経過するまで Sleep() しますか? そんなことをしていたら、 DateTime.Now に依存するメソッドのテストに、 ヘタをすると丸一日掛かってしまうかもしれませんね。

DateTime.Now の値を自由にコントロールできれば、 問題は解決するでしょう。
製品コードの中で、 現在時刻を提供する部分を切り出してしまい、 テストのときはそこをスリ換えてしまうというテクニックがあります。
・ 製品コードに、 現在時刻を提供するクラスを作って、 テストのときはクラスを入れ換える。 (ちょっと大げさだけど、 大きなアプリケーションならけっこう現実的。)
・ 製品クラスの中で、 現在時刻を提供するメソッドかプロパティを切り出して、 テストの時にはそこをすげ替える。
・ 製品クラスの中に、 #if DEBUG で括って、 時刻を調整できるメソッドを仕込んでおく。
・ (…ほかにも方法はあるでしょう。)

以下、 2番目のテクニックを紹介します。

まず、 悪い実装例から。

    public class 良くない挨拶
    {
        public static string 言え(){
            DateTime nowTime = DateTime.Now;
            if (朝ですか(nowTime))
            {
                return "Good morning!";
            }
            return "Hello!";
        }

        private static bool 朝ですか(DateTime nowTime)
        {
            int hour = nowTime.Hour;
            return ((5 <= hour) && (hour <= 10));
        }
    }

これは、 朝には "Good Morning!" と、 それ以外では "Hello!" と挨拶する製品コードです。 TDD でやってれば、 こんなコードにはならない (なぜなら、 DateTime.Now のせいでテストを上手く書けない) のですが、 TDD していないときにはちょくちょくお目に掛かりそうなコードです。
ですがこれでは、 言え() メソッドのテストが上手く書けません。 手動テストなら、 OS の時計をいじってテストできるのですが… ユニットテストでは、 同時にどんなプログラムが動いているかわかりませんから、 そんなことをしたら何が起きるか知れたものではありません。 CI (継続的インテグレーション) でテストの自動実行をさせてたら、 間違いなく問題を引き起こすでしょう。

それでは、 次が DateTime.Now をスリ換えるようにしたユニット テストと製品コードです。 (テストをリファクタリングした結果だと思ってください。)

    [TestClass]
    public class 良い挨拶Test
    {
        // テスト対象のクラスを継承して、 現在時刻を返すプロパティをオーバーライド
        private class テスト用の良い挨拶 : 良い挨拶
        {
            public DateTime DummyTime;
            protected override DateTime 現在時刻
            {
                get{    return DummyTime;    }
            }
        }

        [TestMethod]
        public void TestMethod1()
        {
            // 継承したクラスを使って、 テスト。
            テスト用の良い挨拶 g = new テスト用の良い挨拶();

            g.DummyTime = new DateTime(2009, 12, 21, 5, 0, 0);
            Assert.AreEqual<string>("Good morning!", g.言え());

            g.DummyTime = new DateTime(2009, 12, 21, 11, 0, 0);
            Assert.AreEqual<string>("Hello!", g.言え());
        }
    }

このようにして、 DateTime.Now に依存しているクラスを継承して、 依存している部分をオーバーライドしてしまい、 テストからコントロールできるようにしてやるわけです。
そして、 製品コードは次のようになります。

    public class 良い挨拶
    {
        public string 言え(){
            if (今は朝ですか())
            {
                return "Good morning!";
            }
            return "Hello!";
        }

        protected virtual DateTime 現在時刻
        {
            get{    return DateTime.Now;    }
        }

        private bool 今は朝ですか()
        {
            int hour = 現在時刻.Hour;
            return ((5 <= hour) && (hour <= 10));
        }
    }

最初に載せたコードより長くなってしまっています。 static だったメソッドが、 インスタンス メソッドになっています。 また、 現在時刻プロパティの中身は、 上記のテストコードでは実行されていません。 なんだかマイナスが目に付いてしまいますが、 それはサンプルのためのごく小さなクラスだからです。 実際のもう少し大きくて複雑なクラスでは、 クラスのほとんど全てをユニットテストできるというメリットのほうが上回るでしょう。 上のダメな製品コードでは、 言え() メソッドのテストはせいぜい返値が null ではないことをチェックする程度しか出来なかったことを思い出してください。

このテクニックは、 プログラムの外部に依存する部分を置き換えたり無効にしたりするために良く使います。 現在時刻だけでなく、 ファイルの読み書きや他システムとの連携部分など、 応用範囲は広いです。
TDD Boot Camp のセッションでも、 Lasse さんが実演の中で、 テストに関係無いファイル I/O をしているメソッドを、 同じテクニックを使って無効にしていたと思います。 テストに関係無いところでテストの時間を喰ってしまうのは、 TDD のスピードに影響しますからね。 必要の無いテストファイルを準備したりするテストコードも、 書きたくないですし。

trackback:
http://d.hatena.ne.jp/kaorun55/20091221/1261422891

|

« [イベント] TDD Boot Camp、いっぺんに 30組以上がペアプログラミングする壮観! | トップページ | [イベント] 告知 - Tech Fielders セミナー 東京 [Agile Day 「A Start - アジャイル開発が気になるあなたに贈るセッション&ワークショップ] ( 1/22, 新宿 ) »

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

コメント

コメントを書く



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


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



トラックバック


この記事へのトラックバック一覧です: [コラム] どうやって DateTime.Now を含むメソッドをテストする?:

« [イベント] TDD Boot Camp、いっぺんに 30組以上がペアプログラミングする壮観! | トップページ | [イベント] 告知 - Tech Fielders セミナー 東京 [Agile Day 「A Start - アジャイル開発が気になるあなたに贈るセッション&ワークショップ] ( 1/22, 新宿 ) »