dotnet-xunit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-xunit
dotnet-xunit
xUnit v3 testing framework features for .NET. Covers and attributes, test fixtures (, ), parallel execution configuration, for async setup/teardown, custom assertions, and xUnit analyzers. Includes v2 compatibility notes where behavior differs.
[Fact][Theory]IClassFixtureICollectionFixtureIAsyncLifetimeVersion 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测试框架特性。包含和特性、测试fixtures(、)、并行执行配置、用于异步初始化/销毁的、自定义断言,以及xUnit分析器。如果存在行为差异,会附上v2兼容性说明。
[Fact][Theory]IClassFixtureICollectionFixtureIAsyncLifetime版本假设: 主要针对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 核心差异
| Feature | xUnit v2 | xUnit v3 |
|---|---|---|
| Package | | |
| Runner | | |
| Async lifecycle | | |
| Assert package | Bundled | Separate |
| Parallelism default | Per-collection | Per-collection (same, but configurable per-assembly) |
| Timeout | | |
| Test output | | |
| Returns | Returns |
| Returns | Supports |
| Assertion messages | Optional string parameter on Assert methods | Removed in favor of custom assertions (v3.0); use |
v2 compatibility note: If migrating from v2, replace package with . Most and tests work without changes. The primary migration effort is in (return type changes to ), (strongly typed row format), and removed assertion message parameters.
xunitxunit.v3[Fact][Theory]IAsyncLifetimeValueTask[ClassData]| 功能 | xUnit v2 | xUnit v3 |
|---|---|---|
| 包名 | | |
| 运行器 | | |
| 异步生命周期 | | |
| 断言包 | 内置 | 独立的 |
| 默认并行策略 | 按集合并行 | 按集合并行(相同策略,但支持按程序集配置) |
| 超时设置 | | |
| 测试输出 | | |
| 返回 | 返回 |
| 返回 | 支持 |
| 断言消息 | Assert方法可选字符串参数 | 已移除,推荐使用自定义断言(v3.0);如需显式消息可使用 |
v2兼容性说明: 从v2迁移时,将包替换为即可。大多数和测试无需修改即可运行。主要的迁移工作量集中在(返回类型改为)、(强类型行格式)以及被移除的断言消息参数。
xunitxunit.v3[Fact][Theory]IAsyncLifetimeValueTask[ClassData]Facts and Theories
Facts 和 Theories
[Fact]
-- Single Test Case
[Fact][Fact]
-- 单测试用例
[Fact]Use for tests with no parameters:
[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);
}
}无参数的测试使用标记:
[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][Theory]
-- 参数化测试
[Theory]Use to run the same test logic with different inputs.
[Theory]需要使用不同输入运行相同测试逻辑时使用标记。
[Theory][InlineData]
[InlineData][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>
[MemberData]TheoryData<T>搭配TheoryData<T>
使用[MemberData]
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][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>IClassFixture<T>
-- 单个测试类内共享
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, and return . In v3, they return . When migrating, change the return types accordingly.
IAsyncLifetime.InitializeAsync()DisposeAsync()TaskValueTask当同一个类中的多个测试需要共享高成本资源(数据库连接、配置)时使用:
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中,和返回。在v3中返回,迁移时需要对应修改返回类型。
IAsyncLifetime.InitializeAsync()DisposeAsync()TaskValueTaskICollectionFixture<T>
-- Shared Across Test Classes
ICollectionFixture<T>ICollectionFixture<T>
-- 跨测试类共享
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测试类上的IAsyncLifetime
IAsyncLifetimeFor 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 attribute is its own implicit collection, so by default test classes run in parallel.
[Collection]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 in the test project root:
xunit.runner.jsonjson
{
"$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 or assembly attributes. v3 retains support with the same property names.
xunit.runner.jsonxunit.runner.json在测试项目根目录创建:
xunit.runner.jsonjson
{
"$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中可以通过或程序集属性进行配置。v3保留了对的支持,属性名与v2一致。
xunit.runner.jsonxunit.runner.jsonTest Output
测试输出
ITestOutputHelper
ITestOutputHelperITestOutputHelper
ITestOutputHelperCapture 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与ILogger
集成
ILoggerBridge xUnit output to for integration tests:
Microsoft.Extensions.Loggingcsharp
// 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.Loggingcsharp
// 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使用Assert.Multiple
(xUnit v3)
Assert.MultipleGroup 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: is new in xUnit v3. In v2, use separate assertions -- the test stops at the first failure.
Assert.Multiple将相关断言分组,即使其中一个失败,所有断言仍会被执行:
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兼容性说明: 是xUnit v3新增功能。在v2中使用独立断言,测试会在第一个失败的断言处停止执行。
Assert.MultiplexUnit Analyzers
xUnit分析器
The package (included with xUnit v3) catches common test authoring mistakes at compile time.
xunit.analyzersxunit.analyzersImportant Rules
重要规则
| Rule | Description | Severity |
|---|---|---|
| Test methods should not be skipped | Info |
| Null should not be used for value type parameters | Warning |
| | Warning |
| Constants and literals should be the expected argument | Warning |
| Do not use null check on value type | Warning |
| Do not use | Warning |
| Do not use equality check to check collection size | Warning |
| Do not use | Warning |
| 规则ID | 描述 | 严重级别 |
|---|---|---|
| 测试方法不应被跳过 | 信息 |
| 值类型参数不应使用null | 警告 |
| 同一个 | 警告 |
| 预期值参数应为常量或字面量 | 警告 |
| 不要对值类型做null检查 | 警告 |
| 不要使用 | 警告 |
| 不要使用相等检查判断集合大小 | 警告 |
| 不要使用 | 警告 |
Suppressing Specific Rules
禁用特定规则
In for test projects:
.editorconfigini
[tests/**.cs]在测试项目的中配置:
.editorconfigini
[tests/**.cs]Allow skipped tests during development
开发阶段允许跳过测试
dotnet_diagnostic.xUnit1004.severity = suggestion
---dotnet_diagnostic.xUnit1004.severity = suggestion
---Key Principles
核心原则
- One fact per , one concept per
[Fact]. If a[Theory]tests fundamentally different scenarios, split into separate[Theory]methods.[Fact] - Use for expensive shared resources within a single test class. Use
IClassFixturewhen multiple classes share the same resource.ICollectionFixture - Do not disable parallelism globally. Instead, group tests that share mutable state into named collections.
- Use for async setup/teardown instead of constructors and
IAsyncLifetime. Constructors cannot be async, andIDisposabledoes not await.IDisposable.Dispose() - Keep test data close to the test. Prefer for simple cases. Use
[InlineData]or[MemberData]only when data is complex or shared.[ClassData] - 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也不支持await。IDisposable.Dispose() - 测试数据尽量靠近测试代码。 简单场景优先使用,仅当数据复杂或需要共享时才使用
[InlineData]或[MemberData]。[ClassData] - 所有测试项目都启用xUnit分析器。 它们可以捕获会导致测试误判或不稳定的常见错误。
Agent Gotchas
Agent避坑指南
- Do not use constructor-injected in static methods.
ITestOutputHelperis per-test-instance; store it in an instance field, not a static one.ITestOutputHelper - Do not forget to make fixture classes . xUnit requires fixture types to be public with a public parameterless constructor (or
public). Non-public fixtures cause silent failures.IAsyncLifetime - Do not mix and
[Fact]on the same method. A method is either a fact or a theory, not both.[Theory] - Do not return from async test methods. Return
voidorTask.ValueTasktests report false success because xUnit cannot observe the async completion.async void - Do not use without a matching
[Collection]. An unmatched collection name silently creates an implicit collection with default behavior, defeating the purpose.[CollectionDefinition]
- 不要在静态方法中使用构造函数注入的。
ITestOutputHelper是按测试实例隔离的,将其存储在实例字段中,不要存在静态字段中。ITestOutputHelper - 不要忘记将fixture类设为。 xUnit要求fixture类型是公共的,且有公共无参构造函数(或实现
public)。非公开fixture会导致静默失败。IAsyncLifetime - 不要在同一个方法上同时标注和
[Fact]。 一个方法要么是fact要么是theory,不能同时是两者。[Theory] - 异步测试方法不要返回。 返回
void或Task,ValueTask测试会报告虚假成功,因为xUnit无法观察异步执行的完成状态。async void - 不要使用没有对应的
[CollectionDefinition]。 未匹配的集合名称会静默创建一个使用默认行为的隐式集合,达不到配置的预期目的。[Collection]