65.9K
CodeProject 正在变化。 阅读更多。
Home

使用“yield return”进行实用的单元测试,以提供测试用例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (5投票s)

2012年12月30日

CPOL

3分钟阅读

viewsIcon

40678

downloadIcon

150

如何更好地控制测试过程,减少单元测试中的测试方法数量。

引言

一个测试项目包含单元测试。单元测试包含测试方法。单元测试应该包含多少个测试方法,以及如何命名它们才能仍然像控制整个测试过程一样?本文展示了,只需一个TestMethod1(),你就可以创建涵盖多个测试用例的单元测试,并且不会失去控制。

首先,我将向您展示如何为简单的场景创建测试用例——仅使用常量值。接下来,您将看到为更复杂的对象创建测试用例,这些对象包含对其他对象或服务的引用。

背景  

与其创建多个测试方法,例如TestDivisionIfDivisorIs0()TestDivisionIfDivisorIs1()TestDivisionIfDivisorIsBiggerThanDividend()等,不如创建定义测试环境的测试用例,并将它们一个接一个地提供给测试方法。测试方法在循环中处理测试用例。

简单场景 - 使用常量值

应该测试Calculator.Divide()方法。

public class Calculator
{
    /// <summary>
    /// Divides dividend by the divisor
    /// </summary>
    /// <param name="dividend">is divided by divisor</param>
    /// <param name="divisor">divides the dividend</param>
    /// <returns></returns>
    public int Divide(int dividend, int divisor)
    {
        return dividend/divisor;
    }
}

首先,我们创建一个测试项目并向其中添加一个UnitTest1TestMethod1()会自动创建。

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        //
        // TODO: Add test logic here
        //
    }
}

下一步,我们将测试用例定义为UnitTest1的私有类。

[TestClass]
public class UnitTest1
{
    (...)

    private class TestCase
    {
        public TestCase(int dividend, int divisor, 
            int expectedResult, Type expectedExceptionType)
        {
            Dividend = dividend;
            Divisor = divisor;
            ExpectedResult = expectedResult;
            ExpectedExceptionType = expectedExceptionType;
        }

        public int Dividend { get; set; }
        public int Divisor { get; set; }
        public int ExpectedResult { get; set; }
        public Type ExpectedExceptionType { get; set; }
        public string Description { get; set; }
    }
}

TestCase类包含Divide()方法的输入参数、预期结果以及任何异常类型(如果测试方法应该抛出任何异常)。附加属性Description以人类可读的方式提供有关测试条件的信息,并帮助我们识别测试用例。

接下来,我们创建一个生成测试用例的方法。

private IEnumerable<TestCase> getTestCases()
{
    // both Dividend and Diviser are 0
    var tc = new TestCase(0, 0, 0, typeof (DivideByZeroException));
    tc.Description = "both Dividend and Diviser are 0";
    yield return tc;
}

最后,我们实现TestMethod1()

[TestMethod]
public void TestMethod1()
{
    foreach (var testCase in getTestCases())
    {
        // Create the tested class
        var c = new Calculator();
        try
        {
            // invoke the tested method
            var result = c.Divide(testCase.Dividend, testCase.Divisor);

            // check the result
            Assert.AreEqual(testCase.ExpectedResult, result);
            Assert.IsNull(testCase.ExpectedExceptionType);
        }
        catch (Exception ex)
        {
            // an error has occured
            Assert.IsNotNull(testCase.ExpectedExceptionType);
            Assert.AreEqual(testCase.ExpectedExceptionType, ex.GetType());
        }
    }
}

TestMethod1()getTestCases()中一个接一个地获取测试用例,对于每个测试用例,它初始化测试环境,然后创建被测试类,调用被测试方法,并将预期结果与计算结果进行比较。

通过使用新项目扩展getTestCases()方法,提供新的测试用例非常容易。

private IEnumerable<TestCase> getTestCases()
{
    (...)

    // Dividend is 0, Diviser is > 0
    tc = new TestCase(0, 1, 0, null);
    tc.Description = "Dividend is 0, Diviser is > 0";
    yield return tc;

    // Dividend is > 0, Diviser is 0
    tc = new TestCase(1, 0, 0, typeof(DivideByZeroException));
    tc.Description = "Dividend is > 0, Diviser is 0";
    yield return tc;
} 

通过上述方式,我们从一个地方提供所有测试用例,并以人类可读的方式描述它们,这有助于我们在调试期间查找失败的测试用例。

高级场景 - 使用模拟

上面的例子很简单。测试用例只提供了常量值。想象一下,一个方法根据当前时间进行计算。首先,需要用ITimeProvider替换DateTime.Now才能进行测试。

public class TimeCalculator
{
    private readonly ITimeProvider _timeProvider;

    public TimeCalculator(ITimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public bool IsTodayMonday()
    {
        return _timeProvider.Now.DayOfWeek == DayOfWeek.Monday;
    }
} 

ITimeProvider提供当前时间。

public interface ITimeProvider
{
    DateTime Now { get; }
}

在生产中,我们使用SystemTimeProvider的实例。

public class SystemTimeProvider : ITimeProvider
{
    public DateTime Now
    {
        get { return DateTime.Now; }
    }
} 

但是为了准备测试用例,我们创建一个模拟 - ConstantTimeProvider。这样,我们就可以对每次方法调用使用相同的时间。

public class ConstantTimeProvider : ITimeProvider
{
    public ConstantTimeProvider(DateTime now)
    {
        Now = now;
    }

    public DateTime Now { get; set; }
} 

一个TestCase示例如下所示。为简便起见,已删除预期异常。

private class TestCase
{
    public TestCase(ITimeProvider timeProvider, bool expectedResult, 
        string description)
    {
        TimeProvider = timeProvider;
        ExpectedResult = expectedResult;
        Description = description;
    }

    public ITimeProvider TimeProvider { get; set; }
    public bool ExpectedResult { get; set; }
    public string Description { get; set; }
}

getTestCases()方法的内容。

private IEnumerable<TestCase> getTestCases()
{
    // today is sunday
    var timeProvider = new ConstantTimeProvider(new DateTime(2013, 01, 06));
    var tc = new TestCase(timeProvider, false, "today is sunday");
    yield return tc;

    // today is monday
    timeProvider = new ConstantTimeProvider(new DateTime(2013, 01, 07));
    tc = new TestCase(timeProvider, true, "today is monday");
    yield return tc;
} 

以及TestMethod1()的内容。

public void TestMethod1()
{
    foreach (var tc in getTestCases())
    {
        var calculator = new TimeCalculator(tc.TimeProvider);
        var result = calculator.IsTodayMonday();
        Assert.AreEqual(tc.ExpectedResult, result, 
            string.Format("Expected {0}, {1}", tc.ExpectedResult, tc.Description));
    }
} 

总结 

有人说(感谢反馈),这种方法可以使用NUnit及其TestCaseAttribute来代替。事实上,这可以在简单的场景(除法示例)中实现,但是高级场景(使用ITimeProvider模拟)需要在运行时创建其对象。

乍一看,这种方法可能会产生很大的开销,但是您可以看到这种模式的优势:将每个TestUnit组织成TestCasegetTestCases()TestMethod1(),而不是创建多个测试方法及其辅助方法并随意命名它们。

感谢您的阅读、评论和评价我的文章。

关注点

我主要关注的是C#源代码的优化。如果您对本文有任何建议,请在下方发表评论。如果您喜欢这篇文章,请给予5星评价。如果您有兴趣编写整洁(非混乱)的代码,我建议您访问我最新的开源项目,用于创建模块化的.NET应用程序 - mcmframework.codeplex.com

历史 

  • 2013年1月8日 - 添加高级场景。
  • 2012年12月24日 - 首次发布。
© . All rights reserved.