3、NUnit入门:NUnit框架介绍、安装与配置、[Test]与[TestCase]特性、经典断言与约束模型
3.1 为什么选择NUnit?
在.NET世界里,测试框架有好几个选择。我个人最常用的是NUnit,原因很简单——它够轻量、够灵活,而且社区活跃度一直很高。
NUnit最初是从Java的JUnit移植过来的,但发展到现在,它已经完全是.NET原生的一套东西了。你想想看,一个框架能活这么多年,肯定有它的道理。
我在项目中遇到过好几次这样的情况:团队里有人提议换别的框架,但最后讨论下来,NUnit的稳定性和生态支持还是最让人放心的。说白了,选测试框架就像选工具,顺手最重要。
3.2 安装与配置
安装NUnit现在真的很简单。你只需要在NuGet包管理器里搜一下,或者直接用命令行:
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Microsoft.NET.Test.Sdk
嗯,这里要注意:NUnit3TestAdapter这个包不能少。少了它,你的测试就跑不起来。我曾经因为这个折腾了半小时,最后发现是文档没看仔细。
配置方面,基本上装完就能用。但如果你用的是.NET Framework的老项目,可能需要额外配置一下app.config。我个人习惯在项目根目录放一个.runsettings文件,用来控制测试运行时的行为:
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<NUnit>
<NumberOfTestWorkers>4</NumberOfTestWorkers>
</NUnit>
</RunSettings>
这个配置的意思是让测试并行跑,能节省不少时间。当然,如果你的测试之间有依赖关系,记得把这个值设成1。
3.3 [Test]特性——最基础的测试标记
[Test]是NUnit里最常用的特性。你只要在方法上加上它,这个方法就变成了一个测试用例。
[Test]
public void 加法测试()
{
var result = 1 + 1;
Assert.AreEqual(2, result);
}
是不是很简单?但这里有个坑:测试方法必须是public的,返回值必须是void或者Task。我见过有人写成private,然后跑来问我为什么测试没跑起来……
3.4 [TestCase]特性——数据驱动测试
如果你觉得写多个[Test]方法太麻烦,那[TestCase]就是你的救星。它允许你给同一个测试方法传入不同的参数,跑出不同的测试用例。
[TestCase(1, 2, 3)]
[TestCase(0, 0, 0)]
[TestCase(-1, 1, 0)]
public void 加法测试(int a, int b, int expected)
{
var result = a + b;
Assert.AreEqual(expected, result);
}
你看,三个测试用例,一个方法就搞定了。我个人特别喜欢这个特性,尤其是在做边界值测试的时候。比如测试一个登录功能,你可以把各种合法的、非法的用户名密码都列出来,一个方法全测完。
为什么会这样?因为[TestCase]本质上是在编译时生成了多个测试用例,每个用例独立运行。所以如果一个失败了,不会影响其他的。
3.5 经典断言 vs 约束模型
NUnit提供了两种断言方式:经典断言和约束模型。很多人刚接触时会困惑,到底该用哪个?
经典断言
经典断言就是Assert.AreEqual、Assert.IsTrue这种写法。它很直观,适合快速上手:
Assert.AreEqual(5, result);
Assert.IsTrue(flag);
Assert.IsNotNull(obj);
约束模型
约束模型是NUnit后来引入的,写法上更像自然语言:
Assert.That(result, Is.EqualTo(5));
Assert.That(flag, Is.True);
Assert.That(obj, Is.Not.Null);
我个人更推荐约束模型。为什么?因为它的可读性更好,而且错误信息更清晰。举个例子:
// 经典断言
Assert.AreEqual(5, result);
// 失败时输出:Expected: 5, But was: 3
// 约束模型
Assert.That(result, Is.EqualTo(5));
// 失败时输出:Expected: 5, But was: 3
// 看起来差不多?但约束模型可以链式调用:
Assert.That(result, Is.GreaterThan(0).And.LessThan(10));
你想想看,当测试失败时,清晰的错误信息能帮你省多少排查时间。我在项目中遇到过好几次,就是因为错误信息太模糊,导致定位问题花了半天。
3.6 常用约束速查表
| 约束 | 说明 | 示例 |
|---|---|---|
| Is.EqualTo | 相等判断 | Assert.That(x, Is.EqualTo(5)) |
| Is.Not | 取反 | Assert.That(x, Is.Not.Null) |
| Is.GreaterThan | 大于 | Assert.That(x, Is.GreaterThan(0)) |
| Is.LessThan | 小于 | Assert.That(x, Is.LessThan(10)) |
| Is.True / Is.False | 布尔判断 | Assert.That(flag, Is.True) |
| Is.Null / Is.Not.Null | 空值判断 | Assert.That(obj, Is.Null) |
| Does.Contain | 包含字符串 | Assert.That(str, Does.Contain("hello")) |
| Throws | 异常断言 | Assert.That(() => fn(), Throws.Exception) |
Is.GreaterThan(0).And.LessThan(10),这在做范围判断时特别方便。
3.7 避坑指南
最后,分享几个我踩过的坑:
- 测试方法不能有参数——除非你用了
[TestCase]。我刚开始学的时候,直接在[Test]方法里加参数,结果NUnit直接报错。 - 不要依赖测试执行顺序——NUnit不保证测试按你写的顺序执行。如果你有依赖关系,用
[Order]特性显式指定。 - 断言失败不等于异常——
Assert失败会抛出AssertionException,但它和普通的异常不一样。别用try-catch去捕获它。
我曾经在一个项目里,因为没注意这些细节,导致测试结果一直不稳定。后来排查了半天,才发现是测试之间互相影响了。嗯,从那以后,我写测试都会特别注意隔离性。
[SetUp]或[TearDown]里写复杂的业务逻辑。这些方法应该只做初始化和清理工作。否则,你的测试会变得难以理解和维护。
好了,这一章的内容就到这里。下一章我们会深入NUnit的高级特性,包括参数化测试、组合测试和自定义约束。到时候见!