NUnit 测试快速概览
如何使用 NUnit 编写测试代码
引言
在本文中,我们将考虑测试类的不同方法。
使用的环境是
- NUnit 2.6.2
- Resharper 运行测试
被测试代码
public sealed class Calculator : ICalculator
{
public int Divide(int a, int b)
{
return a/b;
}
}
在这个项目中,我们将测试计算器的三个测试用例
- 1/1 = 1
- 2/1 = 2
- 1/0 无效
简单解决方案
[TestFixture]
public sealed class CalculatorTest1
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[Test]
public void OneDividedByOne()
{
int result = _calculator.Divide(1, 1);
Assert.AreEqual(1, result);
}
[Test]
public void TwoDividedByOne()
{
int result = _calculator.Divide(2, 1);
Assert.AreEqual(2, result);
}
[Test]
public void OneDividedByZero()
{
Assert.Throws<DivideByZeroException>(() => _calculator.Divide(1, 0));
}
}
这是你在阅读 NUnit 文档之前可能编写的简单解决方案。
让我们尝试让它更好一些。
使用 TestCase 属性
[TestFixture]
public sealed class CalculatorTest2
{
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
private Calculator _calculator;
[TestCase(1, 1, ExpectedResult = 1, TestName = "OneDividedByOne")]
[TestCase(2, 1, ExpectedResult = 2, TestName = "TwoDividedByOne")]
[TestCase(1, 0, ExpectedResult = 0, ExpectedException = typeof (DivideByZeroException), TestName = "OneDividedByZero")]
public int CalculatorTestMethod(int firstNumber, int secondNumer)
{
return _calculator.Divide(firstNumber, secondNumer);
}
}
使用 TestCase 属性使代码更短,将所有情况分组在一个方法中。
这种编写测试的方法似乎非常适合测试我们的 Calculator
。
添加新的 TestCase
非常快速和清晰。
但是,如果您的被测试方法将引用类型作为参数,而不是字符串,则不能使用 TestCase
属性。
使用 TestCaseSource 属性
[TestFixture]
public sealed class CalculatorTest3
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[TestCaseSource(typeof(CalculatorTest3TestCaseDataFactory), "TestCases")]
public void CalculatorTestMethod(int firstNumber, int secondNumer, int expectedResult)
{
var result = _calculator.Divide(firstNumber, secondNumer);
Assert.AreEqual(expectedResult, result,"A meaning description to help if test crashes");
}
}
以及关联的测试用例数据工厂
public class CalculatorTest3TestCaseDataFactory
{
public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(1, 1, 1).SetName("OneDividedByOne");
yield return new TestCaseData(2, 1, 2).SetName("TwoDividedByOne");
yield return new TestCaseData(1, 0, default(int))
.Throws(typeof(DivideByZeroException))
.SetName("OneDividedByZero");
}
}
}
此实现允许划分关注点
CalculatorTest3
包含测试数据的方式CalculatorTest3TestCaseDataFactory
提供数据
在我们的简单示例中,我们必须使用 Returns()
方法来设置我们的预期结果。
让我告诉你为什么在我被测试的类返回一个引用类型 MyClass
时,我不会使用它,我编写了
- NUnit 将使用
MyClass.Equals
,也许我想以不同的方式比较我的对象 - 我可以在
TestClass
中编码我自己的比较,添加有意义的描述
输入我们的数据
让我们声明 CalculatorTestCaseData
,它将保存我们的类型化数据
public class CalculatorTestCaseData { public int FirstNumber { get; set; } public int SecondNumber { get; set; } public int ExpectedResult { get; set; } }
现在测试类将是
[TestFixture] public sealed class CalculatorTest4 { private Calculator _calculator; [SetUp] public void Setup() { _calculator = new Calculator(); } [TestCaseSource(typeof(CalculatorTestCaseDataFactory4), "GetTestCases")] public void CalculatorTestMethod(CalculatorTestCaseData testCase) { int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber); Assert.AreEqual(testCase.ExpectedResult, result); } }
以及将实例化 CalculatorTestCaseData 的工厂
public class CalculatorTestCaseDataFactory4 { public IEnumerable GetTestCases { get { yield return new TestCaseData(OneDividedByOne()).SetName("OneDividedByOne"); yield return new TestCaseData(TwoDividedByOne()).SetName("TwoDividedByOne"); yield return new TestCaseData(OneDividedByZero()) .Throws(typeof(DivideByZeroException)) .SetName("OneDividedByZero"); } } private CalculatorTestCaseData OneDividedByOne() { return new CalculatorTestCaseData { FirstNumber = 1, SecondNumber = 1, ExpectedResult = 1 }; } private CalculatorTestCaseData TwoDividedByOne() { return new CalculatorTestCaseData { FirstNumber = 2, SecondNumber = 1, ExpectedResult = 2 }; } private CalculatorTestCaseData OneDividedByZero() { return new CalculatorTestCaseData { FirstNumber = 1, SecondNumber = 0 }; } }
我们现在正在操作类型化的数据,测试用例比之前的示例更有意义。
此示例中的一个缺点是测试用例名称和方法名称相同,不幸的是它不是重构证明的(如果您想更改名称,则必须更改每个名称)。
让我们用下一个例子来解决它。
使其成为重构证明
[TestFixture] public sealed class CalculatorTest5 { private Calculator _calculator; [SetUp] public void Setup() { _calculator = new Calculator(); } [TestCaseSource(typeof(CalculatorTestCaseDataFactory5), "GetTestCases")] public void CalculatorTestMethod(CalculatorTestCaseData testCase) { int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber); Assert.AreEqual(testCase.ExpectedResult, result); } }
使用数据工厂
public class CalculatorTestCaseDataFactory5 { private static readonly TestCaseDataFactory<CalculatorTestCaseData> TestCaseDataFactory = new TestCaseDataFactory<CalculatorTestCaseData>(); public IEnumerable GetTestCases { get { yield return OneDividedByOne(); yield return TwoDividedByOne(); yield return OneDividedByZero().Throws(typeof(DivideByZeroException)); } } private TestCaseData OneDividedByOne() { var calculatorTCD = new CalculatorTestCaseData { FirstNumber = 1, SecondNumber = 1, ExpectedResult = 1 }; return TestCaseDataFactory.Get(calculatorTCD); } private TestCaseData TwoDividedByOne() { var calculatorTCD = new CalculatorTestCaseData { FirstNumber = 2, SecondNumber = 1, ExpectedResult = 2 }; return TestCaseDataFactory.Get(calculatorTCD); } private TestCaseData OneDividedByZero() { var calculatorTCD = new CalculatorTestCaseData { FirstNumber = 1, SecondNumber = 0 }; return TestCaseDataFactory.Get(calculatorTCD); } }
和 TestCaseDataFactory
public sealed class TestCaseDataFactory<T>
{
public TestCaseData Get(T data, [CallerMemberName] string memberName = "noName")
{
return new TestCaseData(data).SetName(memberName);
}
}
我们获得了之前版本的所有优点,而没有名称的重复。 您将找到附加的 Resharper 模板,用于制作此版本的测试。
通过 TestFixture 传递参数
现在让我们想象一下,我们有另一个 ICalculator
的实现,它对于相同的输入工作方式完全相同: SlowCalculator
。 我们将不得不使用相同的测试用例(前一部分的测试用例)来测试它。 一种干净的方法是将参数传递给 TestFixture
属性,如下所示。
[TestFixture(CalculatorType.Standard)] [TestFixture(CalculatorType.Slow)] public sealed class CalculatorTest6 { private ICalculator _calculator; private readonly CalculatorType _calculatorType; private readonly CalculatorFactory _calculatorFactory; public CalculatorTest6(CalculatorType calculatorType) { _calculatorType = calculatorType; _calculatorFactory = new CalculatorFactory(); } [SetUp] public void Setup() { _calculator = _calculatorFactory.Get(_calculatorType); } [TestCaseSource(typeof(CalculatorTestCaseDataFactory5), "GetTestCases")] public void CalculatorTestMethod(CalculatorTestCaseData testCase) { int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber); Assert.AreEqual(testCase.ExpectedResult, result); } }
CalculatorFactory
public class CalculatorFactory { public ICalculator Get(CalculatorType calculatorType) { switch (calculatorType) { case CalculatorType.Standard: return new Calculator(); case CalculatorType.Slow: return new SlowCalculator(); } string message = String.Format("No implementation matching for type {0}. Please add it", calculatorType); throw new ArgumentException(message); } }
三个测试用例将应用于正常的 Calculator
,然后应用于我们的新 SlowCalculator
我们的测试执行的输出
结论
我们已经看到了使用 NUnit 编码单元测试的各种版本。
关于你必须测试的类,你将不得不选择合适的方式。 使用 Calculator
示例,使用 TestCase 属性的那个似乎很有效且更简单。
在更困难的示例中,TestCaseSource 可以帮助你获得适应的代码覆盖率。
所以现在你没有更多的借口来跳过编码测试,冲啊冲啊冲。