第2章:xUnit入门:框架介绍、安装与配置、[Fact]与[Theory]特性、Assert断言类详解
2.1 xUnit框架是什么?
说实话,.NET生态里的测试框架不少。NUnit是老牌选手,MSTest是微软亲儿子。但xUnit.net,我个人觉得是最「现代」的一个。
它由NUnit的原作者Brad Wilson和James Newkirk共同打造。说白了,就是吸取了前两代框架的教训,重新设计了一套更干净、更可扩展的测试框架。
我在项目中遇到过好几次,团队从MSTest迁移到xUnit后,测试代码的维护成本明显降低了。为什么?因为xUnit的设计哲学就是「约定优于配置」,你不需要继承某个基类,也不需要给每个方法贴一堆属性。
核心特点:
- 轻量级,不强制继承任何基类
- 扩展性极强,支持自定义特性
- 并行执行测试,跑得飞快
- 与.NET Core/5+ 深度集成
2.2 安装与配置
安装xUnit非常简单。你可以在Visual Studio里通过NuGet包管理器操作,也可以用命令行。我个人习惯用dotnet CLI,因为快,而且跨平台一致。
dotnet new xunit -n MyFirstTestProject
cd MyFirstTestProject
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
嗯,这里要注意:xunit.runner.visualstudio 这个包是必须的,否则你在VS的测试资源管理器里看不到测试结果。我曾经有一次忘了加这个包,折腾了半小时才发现问题。
配置方面,xUnit基本是零配置。你只需要在项目文件里确认一下目标框架:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
小提示:如果你用的是.NET Framework 4.x,记得把 TargetFramework 改成 net472 之类的版本。xUnit对老框架的支持也很完善。
2.3 [Fact] 与 [Theory] 特性
这两个特性是xUnit的灵魂。你想想看,写测试的时候,最烦的是什么?重复代码。一个方法要测多组数据,就得写多个测试方法,太累了。
[Fact] 是最基本的测试特性。它表示这是一个「事实」——也就是说,这个测试方法不接受任何参数,只验证一个确定的场景。
[Fact]
public void Add_ShouldReturnSumOfTwoNumbers()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
[Theory] 就不一样了。它允许你传入参数,用同一套逻辑验证多组数据。配合 [InlineData] 使用,效果拔群。
[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
我在项目中遇到过,有人用[Fact]写了十几个几乎一样的测试方法,就为了测不同的输入。后来我建议他改成[Theory],代码量直接砍掉三分之二。你想想看,维护起来多轻松。
注意:[Theory] 虽然好用,但别滥用。如果你的测试逻辑本身就很复杂,或者每个输入需要不同的准备步骤,那还是老老实实写多个[Fact]吧。可读性比代码量更重要。
2.4 Assert断言类详解
Assert类是你写测试时最亲密的伙伴。说白了,它就是用来「断言」某个条件是否成立的。如果断言失败,测试就红了。
我刚开始用xUnit时,总觉得Assert的方法太多了,记不住。后来发现,常用的其实就那么几个。我整理了一张表,你收藏一下:
| 断言方法 | 用途 | 示例 |
|---|---|---|
| Assert.Equal | 判断两个值是否相等 | Assert.Equal(5, result) |
| Assert.NotEqual | 判断两个值是否不相等 | Assert.NotEqual(0, result) |
| Assert.True | 判断条件是否为真 | Assert.True(isValid) |
| Assert.False | 判断条件是否为假 | Assert.False(hasError) |
| Assert.Null | 判断对象是否为null | Assert.Null(user) |
| Assert.NotNull | 判断对象是否不为null | Assert.NotNull(user) |
| Assert.Throws | 判断是否抛出了指定异常 | Assert.Throws<DivideByZeroException>(() => calc.Divide(1, 0)) |
| Assert.Contains | 判断集合是否包含某元素 | Assert.Contains("apple", fruits) |
| Assert.DoesNotContain | 判断集合是否不包含某元素 | Assert.DoesNotContain("banana", fruits) |
| Assert.Empty | 判断集合是否为空 | Assert.Empty(list) |
| Assert.NotEmpty | 判断集合是否不为空 | Assert.NotEmpty(list) |
这里我要特别提一下 Assert.Throws。我曾经在项目中遇到一个坑:测试异常场景时,有人直接用 try-catch 包裹,然后手动判断。其实完全没必要,Assert.Throws 就是干这个的,而且它还会返回异常对象,方便你进一步验证异常消息。
[Fact]
public void Divide_ByZero_ShouldThrowException()
{
var calculator = new Calculator();
var ex = Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
Assert.Equal("除数不能为零", ex.Message);
}
小技巧:断言浮点数时,别直接用 Assert.Equal,因为浮点精度问题会导致测试不稳定。用 Assert.Equal(expected, actual, precision) 指定精度,比如 Assert.Equal(0.333, result, 3)。
2.5 实战小案例:写一个完整的测试
光说不练假把式。我们拿一个简单的计算器类来练手:
public class Calculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
public int Multiply(int a, int b) => a * b;
public int Divide(int a, int b) => a / b;
}
对应的测试类:
public class CalculatorTests
{
private readonly Calculator _calculator;
public CalculatorTests()
{
_calculator = new Calculator();
}
[Fact]
public void Add_ShouldReturnSum()
{
var result = _calculator.Add(2, 3);
Assert.Equal(5, result);
}
[Theory]
[InlineData(10, 2, 5)]
[InlineData(9, 3, 3)]
[InlineData(7, 1, 7)]
public void Divide_ShouldReturnQuotient(int a, int b, int expected)
{
var result = _calculator.Divide(a, b);
Assert.Equal(expected, result);
}
[Fact]
public void Divide_ByZero_ShouldThrow()
{
Assert.Throws<DivideByZeroException>(() => _calculator.Divide(1, 0));
}
}
你看,这个测试类里既有[Fact]又有[Theory],既有普通断言又有异常断言。跑一下,全绿。嗯,这种感觉很爽。
总结一下:
- xUnit是.NET生态里最现代化的测试框架,值得投入学习
- [Fact]用于无参数的单场景测试,[Theory]用于多参数的数据驱动测试
- Assert类提供了丰富的断言方法,掌握常用的10个左右就够用了
- 测试代码也是代码,要注重可读性和可维护性
下一章,我们会深入聊一聊「测试生命周期」——也就是构造函数、Dispose、IClassFixture这些东西。这些东西在实际项目中特别重要,尤其是当你需要共享数据库连接或者模拟服务的时候。到时候见。