第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这些东西。这些东西在实际项目中特别重要,尤其是当你需要共享数据库连接或者模拟服务的时候。到时候见。