⚡

dotnet-xunit测试框架Skill dotnet-xunit

此技能专注于在 .NET 环境中使用 xUnit v3 测试框架,涵盖 Facts、Theories、测试夹具、并行执行、IAsyncLifetime、自定义断言等关键功能,帮助开发者编写单元测试和集成测试,提高代码质量和测试覆盖率,适用于 SEO 关键词如 xUnit v3、.NET 测试、单元测试、参数化测试、测试夹具、并行测试、异步生命周期。

测试 0 次安装 33 次浏览 更新于 3/6/2026

名称: dotnet-xunit 描述: “编写 xUnit v3 测试:事实、理论、夹具、并行性、IAsyncLifetime。” 用户可调用: false

dotnet-xunit

.NET 的 xUnit v3 测试框架功能。涵盖 [Fact] 和 [Theory] 属性、测试夹具(IClassFixture、ICollectionFixture)、并行执行配置、用于异步设置/清理的 IAsyncLifetime、自定义断言和 xUnit 分析器。包括 v2 兼容性说明,其中行为不同。

版本假设: xUnit v3 为主要版本(.NET 8.0+ 基线)。在 v3 行为与 v2 不同的地方,提供内联兼容性说明。xUnit v2 仍被广泛使用;许多项目在迁移期间会遇到两个版本。

范围

  • [Fact] 和 [Theory] 测试属性及数据源
  • 测试夹具(IClassFixture、ICollectionFixture)和共享上下文
  • 并行执行配置和集合排序
  • IAsyncLifetime 用于异步设置/清理
  • xUnit 分析器和自定义断言
  • 从 xUnit v2 迁移到 v3(TheoryDataRow、ValueTask 生命周期)

超出范围

  • 测试项目脚手架 – 参见 [skill:dotnet-add-testing]
  • 测试策略和测试类型决策 – 参见 [skill:dotnet-testing-strategy]
  • 集成测试模式(WebApplicationFactory、Testcontainers) – 参见 [skill:dotnet-integration-testing]
  • 使用 Verify 的快照测试 – 参见 [skill:dotnet-snapshot-testing]

先决条件: 测试项目已通过 [skill:dotnet-add-testing] 脚手架,并引用了 xUnit 包。运行 [skill:dotnet-version-detection] 以确认 .NET 8.0+ 基线以支持 xUnit v3。

交叉引用: [skill:dotnet-testing-strategy] 用于决定测试内容和方式,[skill:dotnet-integration-testing] 用于结合 xUnit 与 WebApplicationFactory 和 Testcontainers。


xUnit v3 与 v2:关键变化

特性 xUnit v2 xUnit v3
包 xunit (2.x) xunit.v3
运行器 xunit.runner.visualstudio xunit.runner.visualstudio (3.x)
异步生命周期 IAsyncLifetime IAsyncLifetime(现在返回 ValueTask)
断言包 捆绑 独立的 xunit.v3.assert(或 xunit.v3.assert.source 用于扩展)
并行默认值 每集合 每集合(相同,但可按程序集配置)
超时 [Fact] 和 [Theory] 上的 Timeout 属性 [Fact] 和 [Theory] 上的 Timeout 属性(未变)
测试输出 ITestOutputHelper ITestOutputHelper(未变)
[ClassData] 返回 IEnumerable<object[]> 返回 IEnumerable<TheoryDataRow<T>>(强类型)
[MemberData] 返回 IEnumerable<object[]> 支持 TheoryData<T> 和 TheoryDataRow<T>
断言消息 Assert 方法上的可选字符串参数 为支持自定义断言而移除(v3.0);使用 Assert.Fail() 获取显式消息

v2 兼容性说明: 如果从 v2 迁移,将 xunit 包替换为 xunit.v3。大多数 [Fact] 和 [Theory] 测试无需更改即可工作。主要的迁移工作在 IAsyncLifetime(返回类型更改为 ValueTask)、[ClassData](强类型行格式)和移除的断言消息参数上。


事实和理论

[Fact] – 单测试用例

使用 [Fact] 用于无参数的测试:

public class DiscountCalculatorTests
{
    [Fact]
    public void Apply_NegativePercentage_ThrowsArgumentOutOfRangeException()
    {
        var calculator = new DiscountCalculator();

        var ex = Assert.Throws<ArgumentOutOfRangeException>(
            () => calculator.Apply(100m, percentage: -5));

        Assert.Equal("percentage", ex.ParamName);
    }

    [Fact]
    public async Task ApplyAsync_ValidDiscount_ReturnsDiscountedPrice()
    {
        var calculator = new DiscountCalculator();

        var result = await calculator.ApplyAsync(100m, percentage: 15);

        Assert.Equal(85m, result);
    }
}

[Theory] – 参数化测试

使用 [Theory] 以相同测试逻辑运行不同输入。

[InlineData]

最适合简单值类型:

[Theory]
[InlineData(100, 10, 90)]      // 10% 折扣 100 = 90
[InlineData(200, 25, 150)]     // 25% 折扣 200 = 150
[InlineData(50, 0, 50)]        // 0% 折扣 = 无变化
[InlineData(100, 100, 0)]      // 100% 折扣 = 0
public void Apply_VariousInputs_ReturnsExpectedPrice(
    decimal price, decimal percentage, decimal expected)
{
    var calculator = new DiscountCalculator();

    var result = calculator.Apply(price, percentage);

    Assert.Equal(expected, result);
}

[MemberData] with TheoryData<T>

最适合复杂数据或共享数据集:

public class OrderValidatorTests
{
    public static TheoryData<Order, bool> ValidationCases => new()
    {
        { new Order { Items = [new("SKU-1", 1)], CustomerId = "C1" }, true },
        { new Order { Items = [], CustomerId = "C1" }, false },              // 无项目
        { new Order { Items = [new("SKU-1", 1)], CustomerId = "" }, false }, // 无客户
    };

    [Theory]
    [MemberData(nameof(ValidationCases))]
    public void IsValid_VariousOrders_ReturnsExpected(Order order, bool expected)
    {
        var validator = new OrderValidator();

        var result = validator.IsValid(order);

        Assert.Equal(expected, result);
    }
}

[ClassData]

最适合在多个测试类之间共享的数据:

// xUnit v3: 使用 TheoryDataRow<T> 获取强类型行
public class CurrencyConversionData : IEnumerable<TheoryDataRow<string, string, decimal>>
{
    public IEnumerator<TheoryDataRow<string, string, decimal>> GetEnumerator()
    {
        yield return new("USD", "EUR", 0.92m);
        yield return new("GBP", "USD", 1.27m);
        yield return new("EUR", "GBP", 0.86m);
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// xUnit v2 兼容性: v2 使用 IEnumerable<object[]> 而不是 TheoryDataRow<T>
// public class CurrencyConversionData : IEnumerable<object[]>
// {
//     public IEnumerator<object[]> GetEnumerator()
//     {
//         yield return new object[] { "USD", "EUR", 0.92m };
//     }
//     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// }

[Theory]
[ClassData(typeof(CurrencyConversionData))]
public void Convert_KnownPairs_ReturnsExpectedRate(
    string from, string to, decimal expectedRate)
{
    var converter = new CurrencyConverter();

    var rate = converter.GetRate(from, to);

    Assert.Equal(expectedRate, rate, precision: 2);
}

夹具:共享设置和清理

夹具在测试之间提供共享的昂贵资源,同时保持测试隔离。

IClassFixture<T> – 每个测试类共享

当同一类中的多个测试共享昂贵资源(数据库连接、配置)时使用:

public class DatabaseFixture : IAsyncLifetime
{
    public string ConnectionString { get; private set; } = "";

    public ValueTask InitializeAsync()
    {
        // xUnit v3: 返回 ValueTask (v2 返回 Task)
        ConnectionString = $"Host=localhost;Database=test_{Guid.NewGuid():N}";
        // 创建数据库,运行迁移等。
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        // xUnit v3: 返回 ValueTask (v2 返回 Task)
        // 删除数据库
        return ValueTask.CompletedTask;
    }
}

public class OrderRepositoryTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _db;

    public OrderRepositoryTests(DatabaseFixture db)
    {
        _db = db;
        // 每个测试获取共享的数据库夹具
    }

    [Fact]
    public async Task GetById_ExistingOrder_ReturnsOrder()
    {
        var repo = new OrderRepository(_db.ConnectionString);

        var result = await repo.GetByIdAsync(KnownOrderId);

        Assert.NotNull(result);
    }
}

v2 兼容性说明: 在 xUnit v2 中,IAsyncLifetime.InitializeAsync() 和 DisposeAsync() 返回 Task。在 v3 中,它们返回 ValueTask。迁移时,相应地更改返回类型。

ICollectionFixture<T> – 跨测试类共享

当多个测试类需要相同昂贵资源时使用:

// 1. 定义集合
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // 此类无代码 -- 它是集合的标记
}

// 2. 在测试类中使用
[Collection("Database")]
public class OrderRepositoryTests
{
    private readonly DatabaseFixture _db;

    public OrderRepositoryTests(DatabaseFixture db)
    {
        _db = db;
    }

    [Fact]
    public async Task Insert_ValidOrder_Persists()
    {
        // 使用共享的数据库夹具
    }
}

[Collection("Database")]
public class CustomerRepositoryTests
{
    private readonly DatabaseFixture _db;

    public CustomerRepositoryTests(DatabaseFixture db)
    {
        _db = db;
    }
}

IAsyncLifetime on Test Classes

用于每个测试的异步设置/清理,无需共享夹具:

public class FileProcessorTests : IAsyncLifetime
{
    private string _tempDir = "";

    public ValueTask InitializeAsync()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(_tempDir);
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        if (Directory.Exists(_tempDir))
            Directory.Delete(_tempDir, recursive: true);
        return ValueTask.CompletedTask;
    }

    [Fact]
    public async Task Process_CsvFile_ExtractsRecords()
    {
        var filePath = Path.Combine(_tempDir, "data.csv");
        await File.WriteAllTextAsync(filePath, "Name,Age
Alice,30
Bob,25");

        var processor = new FileProcessor();
        var records = await processor.ProcessAsync(filePath);

        Assert.Equal(2, records.Count);
    }
}

并行执行

默认行为

xUnit 在集合内顺序运行测试类,但并行运行不同集合。没有显式 [Collection] 属性的每个测试类都是其自己的隐式集合,因此默认情况下测试类并行运行。

控制并行性

为特定测试禁用并行性

将共享可变状态的测试放在同一集合中:

[CollectionDefinition("Sequential", DisableParallelization = true)]
public class SequentialCollection { }

[Collection("Sequential")]
public class StatefulServiceTests
{
    // 这些测试在此集合内顺序运行
}

程序集级配置

在测试项目根目录创建 xunit.runner.json:

{
    "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
    "parallelizeAssembly": false,
    "parallelizeTestCollections": true,
    "maxParallelThreads": 4
}

确保复制到输出:

<ItemGroup>
  <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

v2 兼容性说明: 在 v2 中,配置通过 xunit.runner.json 或程序集属性完成。v3 保留 xunit.runner.json 支持,属性名相同。


测试输出

ITestOutputHelper

捕获测试结果中显示的诊断输出:

public class DiagnosticTests
{
    private readonly ITestOutputHelper _output;

    public DiagnosticTests(ITestOutputHelper output)
    {
        _output = output;
    }

    [Fact]
    public async Task ProcessBatch_LargeDataset_CompletesWithinTimeout()
    {
        var sw = Stopwatch.StartNew();

        var result = await processor.ProcessBatchAsync(largeDataset);

        sw.Stop();
        _output.WriteLine($"Processed {result.Count} items in {sw.ElapsedMilliseconds}ms");
        Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5));
    }
}

与 ILogger 集成

将 xUnit 输出桥接到 Microsoft.Extensions.Logging 用于集成测试:

// NuGet: Microsoft.Extensions.Logging(用于 LoggerFactory)
// + 写入 ITestOutputHelper 的日志提供程序
// 常见方法:使用简单适配器
public class XunitLoggerProvider : ILoggerProvider
{
    private readonly ITestOutputHelper _output;

    public XunitLoggerProvider(ITestOutputHelper output) => _output = output;

    public ILogger CreateLogger(string categoryName) =>
        new XunitLogger(_output, categoryName);

    public void Dispose() { }
}

public class XunitLogger(ITestOutputHelper output, string category) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
        Exception? exception, Func<TState, Exception?, string> formatter)
    {
        output.WriteLine($"[{logLevel}] {category}: {formatter(state, exception)}");
        if (exception is not null)
            output.WriteLine(exception.ToString());
    }
}

自定义断言

扩展 Assert 与自定义方法

创建领域特定断言以获取更简洁的测试代码:

public static class OrderAssert
{
    public static void HasStatus(Order order, OrderStatus expected)
    {
        Assert.NotNull(order);
        if (order.Status != expected)
        {
            throw Xunit.Sdk.EqualException.ForMismatchedValues(
                expected, order.Status);
        }
    }

    public static void ContainsItem(Order order, string sku, int quantity)
    {
        Assert.NotNull(order);
        var item = Assert.Single(order.Items, i => i.Sku == sku);
        Assert.Equal(quantity, item.Quantity);
    }
}

// 用法
[Fact]
public void Complete_ValidOrder_SetsCompletedStatus()
{
    var order = new Order();
    order.Complete();

    OrderAssert.HasStatus(order, OrderStatus.Completed);
}

使用 Assert.Multiple (xUnit v3)

分组相关断言,以便即使一个失败也能评估所有:

[Fact]
public void CreateOrder_ValidRequest_SetsAllProperties()
{
    var order = OrderFactory.Create(request);

    Assert.Multiple(
        () => Assert.Equal("cust-123", order.CustomerId),
        () => Assert.Equal(OrderStatus.Pending, order.Status),
        () => Assert.NotEqual(Guid.Empty, order.Id),
        () => Assert.NotEmpty(order.Items)
    );
}

v2 兼容性说明: Assert.Multiple 是 xUnit v3 中的新特性。在 v2 中,使用单独的断言 – 测试在第一个失败处停止。


xUnit 分析器

xunit.analyzers 包(随 xUnit v3 包含)在编译时捕获常见的测试编写错误。

重要规则

规则 描述 严重性
xUnit1004 测试方法不应被跳过 信息
xUnit1012 不应为值类型参数使用 null 警告
xUnit1025 InlineData 在 Theory 内应唯一 警告
xUnit2000 常量和字面量应为预期参数 警告
xUnit2002 不要在值类型上进行 null 检查 警告
xUnit2007 不要使用 typeof 表达式检查类型 警告
xUnit2013 不要使用相等检查来检查集合大小 警告
xUnit2017 不要使用 Contains() 检查值是否存在于集合中 警告

抑制特定规则

在测试项目的 .editorconfig 中:

[tests/**.cs]
# 允许开发期间跳过的测试
dotnet_diagnostic.xUnit1004.severity = suggestion

关键原则

  • 每个 [Fact] 一个事实,每个 [Theory] 一个概念。 如果 [Theory] 测试根本不同的场景,拆分为单独的 [Fact] 方法。
  • 使用 IClassFixture 用于昂贵共享资源 在单个测试类内。使用 ICollectionFixture 当多个类共享相同资源时。
  • 不要全局禁用并行性。 相反,将共享可变状态的测试分组到命名集合中。
  • 使用 IAsyncLifetime 用于异步设置/清理 而不是构造函数和 IDisposable。构造函数不能是异步的,IDisposable.Dispose() 不等待。
  • 保持测试数据接近测试。 首选 [InlineData] 用于简单情况。仅当数据复杂或共享时使用 [MemberData] 或 [ClassData]。
  • 在所有测试项目中启用 xUnit 分析器。 它们捕获导致假通过或不稳定测试的常见错误。

代理注意事项

  1. 不要在静态方法中使用构造函数注入的 ITestOutputHelper。 ITestOutputHelper 是每个测试实例的;存储在实例字段中,而不是静态字段。
  2. 不要忘记使夹具类 public。 xUnit 要求夹具类型为 public,具有 public 无参数构造函数(或 IAsyncLifetime)。非 public 夹具导致静默失败。
  3. 不要在同一方法上混合 [Fact] 和 [Theory]。 方法是事实或理论,不能两者都是。
  4. 不要从异步测试方法返回 void。 返回 Task 或 ValueTask。async void 测试报告假成功,因为 xUnit 无法观察异步完成。
  5. 不要使用 [Collection] 没有匹配的 [CollectionDefinition]。 未匹配的集合名称静默创建具有默认行为的隐式集合,违背目的。

参考