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






4.33/5 (5投票s)
如何更好地控制测试过程,减少单元测试中的测试方法数量。
引言
一个测试项目包含单元测试。单元测试包含测试方法。单元测试应该包含多少个测试方法,以及如何命名它们才能仍然像控制整个测试过程一样?本文展示了,只需一个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;
}
}
首先,我们创建一个测试项目并向其中添加一个UnitTest1
。TestMethod1()
会自动创建。
[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组织成TestCase
、getTestCases()
和TestMethod1()
,而不是创建多个测试方法及其辅助方法并随意命名它们。
感谢您的阅读、评论和评价我的文章。
关注点
我主要关注的是C#源代码的优化。如果您对本文有任何建议,请在下方发表评论。如果您喜欢这篇文章,请给予5星评价。如果您有兴趣编写整洁(非混乱)的代码,我建议您访问我最新的开源项目,用于创建模块化的.NET应用程序 - mcmframework.codeplex.com。
历史
- 2013年1月8日 - 添加高级场景。
- 2012年12月24日 - 首次发布。