dotnet-xunit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-xunit

dotnet-xunit

xUnit v3 testing framework features for .NET. Covers
[Fact]
and
[Theory]
attributes, test fixtures (
IClassFixture
,
ICollectionFixture
), parallel execution configuration,
IAsyncLifetime
for async setup/teardown, custom assertions, and xUnit analyzers. Includes v2 compatibility notes where behavior differs.
Version assumptions: xUnit v3 primary (.NET 8.0+ baseline). Where v3 behavior differs from v2, compatibility notes are provided inline. xUnit v2 remains widely used; many projects will encounter both versions during migration.
Out of scope: Test project scaffolding (creating xUnit projects, package references, Directory.Build.props) is owned by [skill:dotnet-add-testing]. Testing strategy and test type decisions are covered by [skill:dotnet-testing-strategy]. Integration testing patterns (WebApplicationFactory, Testcontainers) are covered by [skill:dotnet-integration-testing].
Prerequisites: Test project already scaffolded via [skill:dotnet-add-testing] with xUnit packages referenced. Run [skill:dotnet-version-detection] to confirm .NET 8.0+ baseline for xUnit v3 support.
Cross-references: [skill:dotnet-testing-strategy] for deciding what to test and how, [skill:dotnet-integration-testing] for combining xUnit with WebApplicationFactory and Testcontainers.

适用于.NET的xUnit v3测试框架特性。包含
[Fact]
[Theory]
特性、测试fixtures(
IClassFixture
ICollectionFixture
)、并行执行配置、用于异步初始化/销毁的
IAsyncLifetime
、自定义断言,以及xUnit分析器。如果存在行为差异,会附上v2兼容性说明。
版本假设: 主要针对xUnit v3(基于.NET 8.0+)。如果v3与v2行为存在差异,会在对应位置附上兼容性说明。xUnit v2目前仍被广泛使用,很多项目在迁移过程中会同时遇到两个版本。
超出范围: 测试项目脚手架(创建xUnit项目、包引用、Directory.Build.props)由[skill:dotnet-add-testing]覆盖。测试策略和测试类型决策在[skill:dotnet-testing-strategy]中介绍。集成测试模式(WebApplicationFactory、Testcontainers)在[skill:dotnet-integration-testing]中介绍。
前置条件: 已经通过[skill:dotnet-add-testing]搭建好测试项目并引用了xUnit包。运行[skill:dotnet-version-detection]确认.NET版本为8.0+,支持xUnit v3。
交叉引用:如需了解测试内容和测试方式的决策,请参考[skill:dotnet-testing-strategy];如需了解xUnit与WebApplicationFactory、Testcontainers的结合使用,请参考[skill:dotnet-integration-testing]。

xUnit v3 vs v2: Key Changes

xUnit v3 与 v2 核心差异

FeaturexUnit v2xUnit v3
Package
xunit
(2.x)
xunit.v3
Runner
xunit.runner.visualstudio
xunit.runner.visualstudio
(3.x)
Async lifecycle
IAsyncLifetime
IAsyncLifetime
(now returns
ValueTask
)
Assert packageBundledSeparate
xunit.v3.assert
(or
xunit.v3.assert.source
for extensibility)
Parallelism defaultPer-collectionPer-collection (same, but configurable per-assembly)
Timeout
Timeout
property on
[Fact]
and
[Theory]
Timeout
property on
[Fact]
and
[Theory]
(unchanged)
Test output
ITestOutputHelper
ITestOutputHelper
(unchanged)
[ClassData]
Returns
IEnumerable<object[]>
Returns
IEnumerable<TheoryDataRow<T>>
(strongly typed)
[MemberData]
Returns
IEnumerable<object[]>
Supports
TheoryData<T>
and
TheoryDataRow<T>
Assertion messagesOptional string parameter on Assert methodsRemoved in favor of custom assertions (v3.0); use
Assert.Fail()
for explicit messages
v2 compatibility note: If migrating from v2, replace
xunit
package with
xunit.v3
. Most
[Fact]
and
[Theory]
tests work without changes. The primary migration effort is in
IAsyncLifetime
(return type changes to
ValueTask
),
[ClassData]
(strongly typed row format), and removed assertion message parameters.

功能xUnit v2xUnit 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]
(强类型行格式)以及被移除的断言消息参数。

Facts and Theories

Facts 和 Theories

[Fact]
-- Single Test Case

[Fact]
-- 单测试用例

Use
[Fact]
for tests with no parameters:
csharp
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);
    }
}
无参数的测试使用
[Fact]
标记:
csharp
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]
-- Parameterized Tests

[Theory]
-- 参数化测试

Use
[Theory]
to run the same test logic with different inputs.
需要使用不同输入运行相同测试逻辑时使用
[Theory]
标记。

[InlineData]

[InlineData]

Best for simple value types:
csharp
[Theory]
[InlineData(100, 10, 90)]      // 10% off 100 = 90
[InlineData(200, 25, 150)]     // 25% off 200 = 150
[InlineData(50, 0, 50)]        // 0% off = no change
[InlineData(100, 100, 0)]      // 100% off = 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);
}
最适合简单值类型的场景:
csharp
[Theory]
[InlineData(100, 10, 90)]      // 100元打9折 = 90元
[InlineData(200, 25, 150)]     // 200元打75折 = 150元
[InlineData(50, 0, 50)]        // 不打折 = 价格不变
[InlineData(100, 100, 0)]      // 全额减免 = 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>

搭配
TheoryData<T>
使用
[MemberData]

Best for complex data or shared datasets:
csharp
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 },              // no items
        { new Order { Items = [new("SKU-1", 1)], CustomerId = "" }, false }, // no customer
    };

    [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);
    }
}
最适合复杂数据或共享数据集的场景:
csharp
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]

[ClassData]

Best for data shared across multiple test classes:
csharp
// xUnit v3: use TheoryDataRow<T> for strongly-typed rows
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 compatibility: v2 uses IEnumerable<object[]> instead of 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);
}

最适合多个测试类共享数据的场景:
csharp
// 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);
}

Fixtures: Shared Setup and Teardown

Fixtures:共享初始化与销毁逻辑

Fixtures provide shared, expensive resources across tests while maintaining test isolation.
Fixtures用于在测试之间共享成本较高的资源,同时保持测试隔离性。

IClassFixture<T>
-- Shared Per Test Class

IClassFixture<T>
-- 单个测试类内共享

Use when multiple tests in the same class share an expensive resource (database connection, configuration):
csharp
public class DatabaseFixture : IAsyncLifetime
{
    public string ConnectionString { get; private set; } = "";

    public ValueTask InitializeAsync()
    {
        // xUnit v3: returns ValueTask (v2 returns Task)
        ConnectionString = $"Host=localhost;Database=test_{Guid.NewGuid():N}";
        // Create database, run migrations, etc.
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync()
    {
        // xUnit v3: returns ValueTask (v2 returns Task)
        // Drop database
        return ValueTask.CompletedTask;
    }
}

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

    public OrderRepositoryTests(DatabaseFixture db)
    {
        _db = db;
        // Each test gets the shared database fixture
    }

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

        var result = await repo.GetByIdAsync(KnownOrderId);

        Assert.NotNull(result);
    }
}
v2 compatibility note: In xUnit v2,
IAsyncLifetime.InitializeAsync()
and
DisposeAsync()
return
Task
. In v3, they return
ValueTask
. When migrating, change the return types accordingly.
当同一个类中的多个测试需要共享高成本资源(数据库连接、配置)时使用:
csharp
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;
        // 每个测试都可以获取共享的数据库fixture
    }

    [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>
-- Shared Across Test Classes

ICollectionFixture<T>
-- 跨测试类共享

Use when multiple test classes need the same expensive resource:
csharp
// 1. Define the collection
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // This class has no code -- it is a marker for the collection
}

// 2. Use in test classes
[Collection("Database")]
public class OrderRepositoryTests
{
    private readonly DatabaseFixture _db;

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

    [Fact]
    public async Task Insert_ValidOrder_Persists()
    {
        // Uses the shared database fixture
    }
}

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

    public CustomerRepositoryTests(DatabaseFixture db)
    {
        _db = db;
    }
}
当多个测试类需要使用同一个高成本资源时使用:
csharp
// 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()
    {
        // 使用共享的数据库fixture
    }
}

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

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

IAsyncLifetime
on Test Classes

测试类上的
IAsyncLifetime

For per-test async setup/teardown without a shared fixture:
csharp
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\nAlice,30\nBob,25");

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

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

无需共享fixture时,用于单测试级别的异步初始化/销毁:
csharp
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\nAlice,30\nBob,25");

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

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

Parallel Execution

并行执行

Default Behavior

默认行为

xUnit runs test classes within a collection sequentially but runs different collections in parallel. Each test class without an explicit
[Collection]
attribute is its own implicit collection, so by default test classes run in parallel.
xUnit会串行运行同一个集合内的测试类,但会并行运行不同集合的测试。没有显式标注
[Collection]
属性的测试类会默认属于独立的隐式集合,因此默认情况下测试类是并行运行的。

Controlling Parallelism

控制并行策略

Disable Parallelism for Specific Tests

为特定测试禁用并行执行

Place tests that share mutable state in the same collection:
csharp
[CollectionDefinition("Sequential", DisableParallelization = true)]
public class SequentialCollection { }

[Collection("Sequential")]
public class StatefulServiceTests
{
    // These tests run sequentially within this collection
}
将共享可变状态的测试放在同一个集合中:
csharp
[CollectionDefinition("Sequential", DisableParallelization = true)]
public class SequentialCollection { }

[Collection("Sequential")]
public class StatefulServiceTests
{
    // 该集合内的测试会串行运行
}

Assembly-Level Configuration

程序集级别配置

Create
xunit.runner.json
in the test project root:
json
{
    "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
    "parallelizeAssembly": false,
    "parallelizeTestCollections": true,
    "maxParallelThreads": 4
}
Ensure it is copied to output:
xml
<ItemGroup>
  <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
v2 compatibility note: In v2, configuration was via
xunit.runner.json
or assembly attributes. v3 retains
xunit.runner.json
support with the same property names.

在测试项目根目录创建
xunit.runner.json
json
{
    "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
    "parallelizeAssembly": false,
    "parallelizeTestCollections": true,
    "maxParallelThreads": 4
}
确保该文件会被复制到输出目录:
xml
<ItemGroup>
  <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
v2兼容性说明: v2中可以通过
xunit.runner.json
或程序集属性进行配置。v3保留了对
xunit.runner.json
的支持,属性名与v2一致。

Test Output

测试输出

ITestOutputHelper

ITestOutputHelper

Capture diagnostic output that appears in test results:
csharp
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));
    }
}
捕获诊断输出,会显示在测试结果中:
csharp
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));
    }
}

Integrating with
ILogger

ILogger
集成

Bridge xUnit output to
Microsoft.Extensions.Logging
for integration tests:
csharp
// NuGet: Microsoft.Extensions.Logging (for LoggerFactory)
// + a logging provider that writes to ITestOutputHelper
// Common approach: use a simple adapter
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());
    }
}

将xUnit输出桥接到
Microsoft.Extensions.Logging
,用于集成测试:
csharp
// 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());
    }
}

Custom Assertions

自定义断言

Extending Assert with Custom Methods

为Assert扩展自定义方法

Create domain-specific assertions for cleaner test code:
csharp
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);
    }
}

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

    OrderAssert.HasStatus(order, OrderStatus.Completed);
}
创建领域特定的断言,让测试代码更简洁:
csharp
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);
}

Using
Assert.Multiple
(xUnit v3)

使用
Assert.Multiple
(xUnit v3)

Group related assertions so all are evaluated even if one fails:
csharp
[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 compatibility note:
Assert.Multiple
is new in xUnit v3. In v2, use separate assertions -- the test stops at the first failure.

将相关断言分组,即使其中一个失败,所有断言仍会被执行:
csharp
[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 Analyzers

xUnit分析器

The
xunit.analyzers
package (included with xUnit v3) catches common test authoring mistakes at compile time.
xunit.analyzers
包(xUnit v3默认包含)可以在编译时捕获常见的测试编写错误。

Important Rules

重要规则

RuleDescriptionSeverity
xUnit1004
Test methods should not be skippedInfo
xUnit1012
Null should not be used for value type parametersWarning
xUnit1025
InlineData
should be unique within a
Theory
Warning
xUnit2000
Constants and literals should be the expected argumentWarning
xUnit2002
Do not use null check on value typeWarning
xUnit2007
Do not use
typeof
expression to check type
Warning
xUnit2013
Do not use equality check to check collection sizeWarning
xUnit2017
Do not use
Contains()
to check if value exists in a set
Warning
规则ID描述严重级别
xUnit1004
测试方法不应被跳过信息
xUnit1012
值类型参数不应使用null警告
xUnit1025
同一个
Theory
中的
InlineData
应唯一
警告
xUnit2000
预期值参数应为常量或字面量警告
xUnit2002
不要对值类型做null检查警告
xUnit2007
不要使用
typeof
表达式检查类型
警告
xUnit2013
不要使用相等检查判断集合大小警告
xUnit2017
不要使用
Contains()
检查值是否存在于集合中
警告

Suppressing Specific Rules

禁用特定规则

In
.editorconfig
for test projects:
ini
[tests/**.cs]
在测试项目的
.editorconfig
中配置:
ini
[tests/**.cs]

Allow skipped tests during development

开发阶段允许跳过测试

dotnet_diagnostic.xUnit1004.severity = suggestion

---
dotnet_diagnostic.xUnit1004.severity = suggestion

---

Key Principles

核心原则

  • One fact per
    [Fact]
    , one concept per
    [Theory]
    .
    If a
    [Theory]
    tests fundamentally different scenarios, split into separate
    [Fact]
    methods.
  • Use
    IClassFixture
    for expensive shared resources
    within a single test class. Use
    ICollectionFixture
    when multiple classes share the same resource.
  • Do not disable parallelism globally. Instead, group tests that share mutable state into named collections.
  • Use
    IAsyncLifetime
    for async setup/teardown
    instead of constructors and
    IDisposable
    . Constructors cannot be async, and
    IDisposable.Dispose()
    does not await.
  • Keep test data close to the test. Prefer
    [InlineData]
    for simple cases. Use
    [MemberData]
    or
    [ClassData]
    only when data is complex or shared.
  • Enable xUnit analyzers in all test projects. They catch common mistakes that lead to false-passing or flaky tests.

  • 每个
    [Fact]
    仅测试一个点,每个
    [Theory]
    仅测试一个概念。
    如果一个
    [Theory]
    测试了完全不同的场景,拆分为独立的
    [Fact]
    方法。
  • 高成本共享资源在单个测试类内使用
    IClassFixture
    ,跨类共享使用
    ICollectionFixture
  • 不要全局禁用并行执行。 而是将共享可变状态的测试分组到命名集合中。
  • 异步初始化/销毁使用
    IAsyncLifetime
    ,而非构造函数和
    IDisposable
    构造函数不能是异步的,
    IDisposable.Dispose()
    也不支持await。
  • 测试数据尽量靠近测试代码。 简单场景优先使用
    [InlineData]
    ,仅当数据复杂或需要共享时才使用
    [MemberData]
    [ClassData]
  • 所有测试项目都启用xUnit分析器。 它们可以捕获会导致测试误判或不稳定的常见错误。

Agent Gotchas

Agent避坑指南

  1. Do not use constructor-injected
    ITestOutputHelper
    in static methods.
    ITestOutputHelper
    is per-test-instance; store it in an instance field, not a static one.
  2. Do not forget to make fixture classes
    public
    .
    xUnit requires fixture types to be public with a public parameterless constructor (or
    IAsyncLifetime
    ). Non-public fixtures cause silent failures.
  3. Do not mix
    [Fact]
    and
    [Theory]
    on the same method.
    A method is either a fact or a theory, not both.
  4. Do not return
    void
    from async test methods.
    Return
    Task
    or
    ValueTask
    .
    async void
    tests report false success because xUnit cannot observe the async completion.
  5. Do not use
    [Collection]
    without a matching
    [CollectionDefinition]
    .
    An unmatched collection name silently creates an implicit collection with default behavior, defeating the purpose.

  1. 不要在静态方法中使用构造函数注入的
    ITestOutputHelper
    ITestOutputHelper
    是按测试实例隔离的,将其存储在实例字段中,不要存在静态字段中。
  2. 不要忘记将fixture类设为
    public
    xUnit要求fixture类型是公共的,且有公共无参构造函数(或实现
    IAsyncLifetime
    )。非公开fixture会导致静默失败。
  3. 不要在同一个方法上同时标注
    [Fact]
    [Theory]
    一个方法要么是fact要么是theory,不能同时是两者。
  4. 异步测试方法不要返回
    void
    返回
    Task
    ValueTask
    async void
    测试会报告虚假成功,因为xUnit无法观察异步执行的完成状态。
  5. 不要使用没有对应
    [CollectionDefinition]
    [Collection]
    未匹配的集合名称会静默创建一个使用默认行为的隐式集合,达不到配置的预期目的。

References

参考资料