OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

NUnit 列挙子でテストケースを量産する

Qiita C# Advent Calendar 2014 16日目のエントリーになります。

どうも、僕です。ワクワクするコード書いてますか?

NUnitのテストがオールグリーンになる瞬間ってワクワクしますよね。
ということで、NUnitのTestCaseの作り方について。

適当なテストのターゲットとして、以下のようなTimeCheckerクラスの
IsValidTimeメソッドを想定しておきます。
ただ単に時間が0-23の範囲で分が0-59の間であることを確認するだけのもの。

public class TimeChecker  
{  
    public bool IsValidTime(int hour, int minute)  
    {  
        return 0 <= hour && hour <= 23 && 0 <= minute && minute <= 59;  
    }  
}  

ベーシックなテストの書き方からいくと、このメソッドのテストは
たとえば以下のような感じになる。

[TestFixture]  
public class TimeCheckerTest  
{  
    [Test]  
    public void 時刻の値として230分が正しいことを判定する()  
    {  
        Assert.IsTrue(new TimeChecker().IsValidTime(2, 30));  
    }  
    [Test]  
    public void 時刻の値として290分が正しくないことを判定する()  
    {  
        Assert.IsFalse(new TimeChecker().IsValidTime(2, 90));  
    }  
}  

さてここで、NUnit 2.5からはTest Case属性が使えるので、
これを利用しない手はないですね。

TestCaseを使って書き直すとこんな感じ。

[TestFixture]  
public class TimeCheckerTest  
{  
    [TestCase(2, 30, Result = true, TestName = "2:30")]  
    [TestCase(2, 90, Result = false, TestName = "2:90")]  
    public bool 時刻の値が妥当かどうかを判定する(int hour, int minute)  
    {  
        return new TimeChecker().IsValidTime(hour, minute);  
    }  
}  

さらに、TestCase属性でパラメータを指定する代わりに
TestCaseSourceを使えば、コンパイル定数でない値をパラメータに使えたり、
より柔軟な書き方が可能になる。

TestCaseSourceは、メソッドのパラメータの順番通りに値を返すものでされあれば
object[]とかでもいいのだけど、ここはNUnitで用意してくれている
TestCaseDataクラスを使いましょう。

[TestFixture]
public class TimeCheckerTest  
{  
    private static TestCaseData[] isValidTestSource  
        = new[]   
        {  
            new TestCaseData(2, 30).Returns(true).SetName("2:30"),  
            new TestCaseData(2, 90).Returns(false).SetName("2:90"),  
        };  
  
    [TestCaseSource("isValidTestSource")]  
    public bool 時刻の値が妥当かどうかを判定する(int hour, int minute)  
    {  
        return new TimeChecker().IsValidTime(hour, minute);  
    }  
}  

ここからようやく本題。
TestCaseSourceはIEnumerableでさえあればいいので、
たとえばプロパティにして↓こんなふうに書いても良いのだ。

private static IEnumerable<TestCaseData> IsValidTestSource  
{  
    get  
    {  
        yield return new TestCaseData(2, 30).Returns(true).SetName("2:30");  
        yield return new TestCaseData(2, 90).Returns(false).SetName("2:90");  
    }  
}  

んでまあ必要性からいえば、せいぜい
0:00, 23:59, -1:00, 0:-1, 24:59, 23:60
あたりをテストしておけば十分でしょうけど、
んなちいせぇことを言わず、なんなら正常パターン全部並べたって
こんな程度ならたいして実行時間かかんないわけですよ。

テストケース作るのだって全然簡単!

private static IEnumerable<TestCaseData> IsValidTestSource  
{  
    get  
    {  
        for (int h = 0; h < 24; h++)  
        {  
            for (int m = 0; m < 60; m++)  
            {  
                yield return new TestCaseData(h, m)  
                                .Returns(true)  
                                .SetName(h + ":" + m.ToString("D2"));  
            }  
        }  
    }  
}  

実行してみる。

testcase_enumeration

ひゃっはー。1440ケース!

補足ですが、実はNUnitの機能としてパラメータにRangeだとかValuesだとかっていう
属性を指定することができて、今回くらいの例であれば
Enumerateとかするまでもなく同じことが可能だったりする。

ですがまあ、yield returnで動的にテストケースを組み立てていけるよっていうのは
知っておくと多少は便利かと思います。

あと一応注意点としては、「動的に」とか言ったけども
テストが実行されるのは、すべてのテストケースの列挙が終わった後なんで、
同じ変数の値を入れ替えつつyield returnとかやってると
アレ?ってことになりがちです。