dotnet-xunit测试框架 dotnet-xunit

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

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

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

dotnet-xunit

.NET 的 xUnit v3 测试框架功能。涵盖 [Fact][Theory] 属性、测试夹具(IClassFixtureICollectionFixture)、并行执行配置、用于异步设置/清理的 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 InlineDataTheory 内应唯一 警告
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 返回 TaskValueTaskasync void 测试报告假成功,因为 xUnit 无法观察异步完成。
  5. 不要使用 [Collection] 没有匹配的 [CollectionDefinition] 未匹配的集合名称静默创建具有默认行为的隐式集合,违背目的。

参考