关于 dotnet-test-frameworks
供 .NET 开发者和代码审查工具识别测试并遵循框架惯例的参考资料,涵盖 MSTest、xUnit、NUnit 和 TUnit,包括断言、生命周期钩子及集成测试标记。
主要功能
- 01识别测试类和测试方法
- 02对照四种框架的断言 API
- 03识别跳过和忽略标记
- 04识别集成测试及外部依赖
- 05查阅初始化与清理约定
适用场景
- 01分类 .NET 代码库中的测试
- 02建议框架原生的异常断言
- 03检查测试中的外部依赖
- 04识别测试生命周期和跳过行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill dotnet-test-frameworks安装后在 Claude Code 中输入/dotnet-test-frameworks确认已加载。
详细介绍
.NET 测试框架参考
.NET 测试框架(MSTest、xUnit、NUnit、TUnit)的语言专属检测模式。
测试文件识别
| 框架 | 测试类标记 | 测试方法标记 |
|---|---|---|
| MSTest | [TestClass] | [TestMethod]、[DataTestMethod] |
| xUnit | (无——基于约定) | [Fact]、[Theory] |
| NUnit | [TestFixture] | [Test]、[TestCase]、[TestCaseSource] |
| TUnit | (无——基于约定) | [Test] |
各框架的断言 API
| 类别 | MSTest | xUnit | NUnit | TUnit |
|---|---|---|---|---|
| 相等 | Assert.AreEqual | Assert.Equal | Assert.That(x, Is.EqualTo(y)) | await Assert.That(x).IsEqualTo(y) |
| 布尔值 | Assert.IsTrue / Assert.IsFalse | Assert.True / Assert.False | Assert.That(x, Is.True) | await Assert.That(x).IsTrue() / await Assert.That(x).IsFalse() |
| 空值 | Assert.IsNull / Assert.IsNotNull | Assert.Null / Assert.NotNull | Assert.That(x, Is.Null) | await Assert.That(x).IsNull() / await Assert.That(x).IsNotNull() |
| 异常 | Assert.Throws<T>() / Assert.ThrowsExactly<T>() | Assert.Throws<T>() | Assert.That(() => ..., Throws.TypeOf<T>()) | await Assert.That(() => ...).Throws<T>() / await Assert.That(() => ...).ThrowsExactly<T>() |
| 集合 | CollectionAssert.Contains | Assert.Contains | Assert.That(col, Has.Member(x)) | await Assert.That(col).Contains(x) |
| 字符串 | StringAssert.Contains | Assert.Contains(str, sub) | Assert.That(str, Does.Contain(sub)) | await Assert.That(str).Contains(sub) |
| 类型 | Assert.IsInstanceOfType | Assert.IsAssignableFrom | Assert.That(x, Is.InstanceOf<T>()) | await Assert.That(x).IsAssignableTo<T>()(精确类型检查请使用 await Assert.That(x).IsTypeOf<T>()) |
| 结果不确定 | Assert.Inconclusive() | 通过 [Fact(Skip)] 跳过 | Assert.Inconclusive() | Skip.Test("reason")(没有真正的结果不确定状态) |
| 失败 | Assert.Fail() | Assert.Fail()(.NET 10+) | Assert.Fail() | Assert.Fail() |
**TUnit 专属:**断言是异步的,必须使用 await 等待——如果忘记使用 await,断言将不会运行,测试会在未提示的情况下通过。内置分析器会在缺少 await 时发出警告。可以通过 .And / .Or 链式调用组合多个断言,也可以使用 Assert.Multiple() 将它们分组。
第三方断言库:Should*(Shouldly)、.Should()(FluentAssertions / AwesomeAssertions)、Verify()(Verify)。TUnit 还提供可选的 TUnit.Assertions.Should 包,它基于相同的基础设施提供 FluentAssertions 风格的 value.Should().BeEqualTo(...)。
Sleep/Delay 模式
| 模式 | 示例 |
|---|---|
| 线程休眠 | Thread.Sleep(2000) |
| 任务延迟 | await Task.Delay(1000) |
| SpinWait | SpinWait.SpinUntil(() => condition, timeout) |
跳过/忽略注解
| 框架 | 注解 | 带原因 |
|---|---|---|
| MSTest | [Ignore] | [Ignore("reason")] |
| xUnit | [Fact(Skip = "reason")] | (必须提供原因) |
| NUnit | [Ignore("reason")] | (必须提供原因) |
| TUnit | [Skip("reason")] | (必须提供原因;也可用于类和程序集范围,例如 [assembly: Skip("…")]。可在测试中动态跳过:Skip.Test("reason")。) |
| 条件 | #if false / #if NEVER | (无法提供原因) |
异常处理——惯用替代方案
如果测试使用 try/catch 验证异常,请建议使用框架原生的替代方式:
MSTest:
// Instead of try/catch (matches exact type):
var ex = Assert.ThrowsExactly<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.AreEqual("Order must contain at least one item", ex.Message);
// Or (also matches derived types):
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.AreEqual("Order must contain at least one item", ex.Message);
xUnit:
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.Equal("Order must contain at least one item", ex.Message);
NUnit:
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.That(ex.Message, Is.EqualTo("Order must contain at least one item"));
TUnit:
await Assert.That(() => processor.ProcessOrder(emptyOrder))
.Throws<InvalidOperationException>()
.WithMessage("Order must contain at least one item");
// Or, for exact-type matching (no derived types):
await Assert.That(() => processor.ProcessOrder(emptyOrder))
.ThrowsExactly<InvalidOperationException>();
神秘访客 — 常见 .NET 模式
| 异味指标 | 检查内容 |
|---|---|
| 文件系统 | File.ReadAllText、File.Exists、File.WriteAllBytes、Directory.GetFiles、使用硬编码路径的 Path.Combine |
| 数据库 | SqlConnection、DbContext(未使用内存中提供程序)、SqlCommand |
| 网络 | 未重写 HttpMessageHandler 的 HttpClient、WebRequest、TcpClient |
| 环境 | Environment.GetEnvironmentVariable、Environment.CurrentDirectory |
| 可接受 | MemoryStream、StringReader、内存中数据库提供程序、自定义 DelegatingHandler |
集成测试标记
将以下情况识别为集成测试(并相应调整异味严重程度):
- 类名包含
Integration、E2E、EndToEnd或Acceptance [TestCategory("Integration")](MSTest)[Trait("Category", "Integration")](xUnit)[Category("Integration")](NUnit、TUnit)- 项目名称以
.IntegrationTests或.E2ETests结尾
设置/清理方法
| 框架 | 设置 | 清理 |
|---|---|---|
| MSTest | [TestInitialize] 或构造函数 | [TestCleanup] 或 IDisposable.Dispose / IAsyncDisposable.DisposeAsync |
| xUnit | 构造函数 | IDisposable.Dispose / IAsyncDisposable.DisposeAsync |
| NUnit | [SetUp] | [TearDown] |
| TUnit | [Before(Test)] 或构造函数 | [After(Test)] 或 IDisposable.Dispose / IAsyncDisposable.DisposeAsync |
| MSTest(类) | [ClassInitialize] | [ClassCleanup] |
| NUnit(类) | [OneTimeSetUp] | [OneTimeTearDown] |
| xUnit(类) | IClassFixture<T> | fixture 的 Dispose |
| TUnit(类) | [Before(Class)] | [After(Class)] |
| TUnit(程序集) | [Before(Assembly)] | [After(Assembly)] |
| TUnit(会话) | [Before(TestSession)] | [After(TestSession)] |
TUnit 专属: [BeforeEvery(Test)] / [AfterEvery(Test)](以及 Class / Assembly 变体)会在整个测试运行期间,对每个测试/类/程序集运行——适用于全局横切钩子。钩子还可以选择接收上下文对象(TestContext、ClassHookContext 等)和/或 CancellationToken。