关于 migrate-nunit-to-mstest
用于将 NUnit 3 或 4 测试项目迁移到 MSTest v4 的技能,保留目标框架和测试运行器,并指导检查、语义映射与验证。
主要功能
- 01检测 NUnit 版本和测试运行器
- 02指导替换程序包并转换测试
- 03映射生命周期和断言语义
- 04保留数据、筛选与并发行为
- 05规定构建和测试验证步骤
适用场景
- 01将 NUnit 3 或 4 项目迁移到 MSTest
- 02处理特定的 NUnit 到 MSTest 编译问题
- 03转换前检查高风险测试语义
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill migrate-nunit-to-mstest安装后在 Claude Code 中输入/migrate-nunit-to-mstest确认已加载。
附带文件references/mapping-cheatsheet.md
详细介绍
NUnit → MSTest 迁移
将 NUnit 3 或 4 测试转换为 MSTest v4,不更改目标框架或测试平台。成功迁移后,项目应能构建、发现相同的测试用例,并保留通过/失败、生命周期、数据、筛选和并发语义。
范围
仅当项目包含 NUnit 包或源码,且用户希望改用 MSTest 时,才使用此技能。如果项目已使用 MSTest 且不包含 NUnit 测试,应报告无需迁移测试框架,并且不做任何更改。
不要将此框架转换与目标框架升级或 VSTest/MTP 迁移合并进行。完成并验证一项迁移后,再开始另一项。
工作区约定
- 在当前工作目录中搜索项目和源码;绝不在此技能目录下查找用户文件。
- 判断交付要求:“转换此项目”表示编辑、构建并测试;“给我一个计划”表示只作答,不编辑。
- 保留用户要求的项目范围。仅当共享 props 文件负责该范围内的 NUnit 包或运行器配置时,才可更改它。
- 最终回复必须说明源 NUnit 版本、保留的运行器、更改的文件、高风险语义映射以及实际测试数量。
响应模式
- **完整迁移请求:**检查、编辑、构建并运行测试。不要止步于计划。
- **针对性的编译错误或 API 问题:**仅应用相关映射。
- **不支持的目标框架:**更改包之前先停止。MSTest v4 的测试应用要求使用 .NET 8+ 或 .NET Framework 4.6.2+;可提出单独审批的 TFM 升级方案或 MSTest v3 方案。
会改变结果的决策
| 检测到的状态 | 必须采取的操作 |
|---|---|
| 不再有 NUnit 包、命名空间、特性或约束 | 停止,不做任何更改,并运行一次现有测试命令,以确认项目已使用 MSTest 且状态正常。 |
| 源项目使用 VSTest | 保留 VSTest。若仓库负责显式固定 Microsoft.NET.Test.Sdk 版本,则保留并更新该版本;不要引入 MTP 属性。 |
| 源项目使用 MTP | 将 NUnit 专用的 MTP 配置替换为 MSTest MTP 配置。优先使用 MSTest.Sdk;使用元包时,设置 EnableMSTestRunner=true 和 OutputType=Exe。 |
| NUnit 使用默认的 fixture 生命周期 | NUnit 通常会在所有测试用例间共享同一个 fixture 实例;MSTest 则为每个测试创建新的类实例。将必须保持类级共享的状态移至静态字段,并通过 [ClassInitialize] 初始化;或者确认状态是每个测试独有的,再将其保留为实例字段。 |
NUnit 未配置 Parallelizable | 保留串行执行。不要添加 [assembly: Parallelize];NUnit 和 MSTest 默认都按串行方式执行。 |
| NUnit 显式启用了并行执行 | 转换其实际生效的范围和工作线程数。绝不要根据 fixture 级设置推断方法级并行。 |
如需查看详细映射,请在 references/mapping-cheatsheet.md 中搜索项目所用的结构,并且只阅读相关部分。
快速路径
常规迁移分四个阶段完成:一次批量发现、一次编辑、一次 dotnet test 和一份简洁结果。
- 如有现成的 CI/测试结果,请将其作为基线。
- 如果没有测试数量,且项目包含参数化测试、fixture 状态、命名空间设置、重试、自定义特性或显式并行配置,请在编辑前运行基线测试。
- 若
dotnet test已足够,不要分别运行 restore、build 和 test 命令。 - 在行为映射完成之前,不要删除 NUnit 运行器配置。
工作流
1. 确定基线
- 批量读取测试项目、中央包文件、
global.json、.runsettings、testconfig.json和 NUnit 配置。 - 根据
NUnit、NUnit3TestAdapter、NUnit.Analyzers、NUnit.Framework、NUnit.Framework.Legacy和using NUnit.Framework检测 NUnit。 - 根据解析出的包版本,说明源项目使用的是 NUnit 3 还是 4。
- 检测 VSTest 或 MTP,并予以保留。仅在无法确定时使用
platform-detection。 - 记录目标框架;如果 MSTest v4 不支持这些框架,则停止。
- 清点高风险构造:
[TestFixture(...)]、[TestFixtureSource]、[FixtureLifeCycle]、带参数的构造函数[TestCase]、[TestCaseSource]、TestCaseData、[Values]、[Range]、[Random]、[Sequential]、[Pairwise]、自定义数据特性[Theory]、[Datapoint]、[DatapointSource]、自动 bool/enum 数据点、Assume.That[OneTimeSetUp]、[OneTimeTearDown]、[SetUpFixture]、基于继承的设置Assert.That、Assert.Multiple、Assert.Throws、Assert.Catch、集合约束[Parallelizable]、[NonParallelizable]、[LevelOfParallelism]、[Order]、[SingleThreaded][Apartment]、[RequiresThread]、[CancelAfter]、[Timeout][Culture]、[Platform]、[SetCulture]、[SetUICulture][Explicit]、[Repeat]、[Retry]、[MaxTime]、类别、属性、[TestOf][DefaultFloatingPointTolerance]、[NonTestAssembly]和已弃用的 fixture 生命周期别名
2. 替换包,但不切换运行程序
移除要替换的 NUnit 专用包,包括 NUnit、NUnit3TestAdapter、NUnit.Analyzers、NUnit 控制台运行程序包和 NUnit 专用 MTP 适配器。
默认使用从已配置包源解析的当前稳定版 MSTest v4 元包:
<PackageReference Include="MSTest" Version="4.4.0" />
此处固定的版本仅供说明当前发行版;实际迁移时,请解析并固定当前稳定版本。如果源项目自行声明了 Microsoft.NET.Test.Sdk 依赖项,请予以保留。保留 MTP 时,优先使用 MSTest.Sdk;否则使用 EnableMSTestRunner=true 和 OutputType=Exe。
不要更改 TargetFramework。只有在转换相关行为后,才能移除 NUnit .runsettings 适配器设置。
3. 执行机械转换
| NUnit | MSTest |
|---|---|
[TestFixture] 或包含测试的 fixture | [TestClass] |
[Test] | [TestMethod] |
[TestCase(...)] | [TestMethod] + [DataRow(...)] |
[TestCaseSource(nameof(Cases))] | [TestMethod] + [DynamicData(nameof(Cases))] |
[SetUp] / [TearDown] | [TestInitialize] / [TestCleanup] |
[OneTimeSetUp] / [OneTimeTearDown] | static [ClassInitialize] / [ClassCleanup] |
[Category(value)] | [TestCategory(value)] |
[Property(key, value)] | [TestProperty(key, value)] |
[Ignore("reason")] | [Ignore("reason")] 加 [TestMethod] |
[Timeout(ms)] | [Timeout(ms)] 加 [TestMethod] |
[Retry(n)] | 当 n > 1 时使用 [Retry(n - 1)] 加 [TestMethod];当 n = 1 时移除 |
移除 using NUnit.Framework; 和 using NUnit.Framework.Legacy;。使用元包时,添加 using Microsoft.VisualStudio.TestTools.UnitTesting;;MSTest.Sdk 会提供隐式全局 using。
不要机械地将类设为 sealed,也不要扁平化继承的设置方法。
4. 解析语义映射
为步骤 1 中发现的每个高风险结构加载映射速查表。这些规则必须遵守:
- NUnit 的默认
LifeCycle.SingleInstance与 MSTest 每个测试使用独立类实例的行为不同。必须显式保留类共享字段。 [OneTimeSetUp]和[OneTimeTearDown]应转换为静态 MSTest 方法。将它们使用的任何实例状态移到静态持有者中,而不是仅仅添加static。- NUnit 的
Assert.Throws<T>和Throws.TypeOf<T>要求异常类型完全匹配,应映射为Assert.ThrowsExactly<T>。NUnit 的Assert.Catch<T>和Throws.InstanceOf<T>允许派生类型,应映射为Assert.Throws<T>。 - NUnit 的
Is.TypeOf<T>应映射为Assert.IsExactInstanceOfType<T>;Is.InstanceOf<T>应映射为Assert.IsInstanceOfType<T>。 - NUnit 的相等约束会逐项比较序列。应在 MSTest 4.3 及更高版本中使用
Assert.AreSequenceEqual,或使用已物化的列表调用CollectionAssert.AreEqual;切勿对序列使用基于引用的Assert.AreEqual。 TestCase(ExpectedResult=...)和TestCaseData.Returns(...)需要重写目标测试,以断言预期结果。- 在需要时,通过从
DynamicData返回TestDataRow<T>,保留每一行的名称、类别和忽略状态。NUnit 行属性没有直接对应的TestDataRow<T>,需要自定义数据源或明确决定如何处理元数据。 - MSTest 4.4 的组合属性可映射 NUnit 的
[Combinatorial]、[Values]、[Range]和[Random];请添加using Microsoft.VisualStudio.TestTools.UnitTesting.Combinatorial;。构造函数语义有所不同,因此应明确转换 NUnit 的范围和随机边界。[Sequential]仍需要显式行或自定义数据源。 - NUnit 的
[Pairwise]没有 MSTest 内置对应项。应在DynamicData中保留完全相同的生成行,或使用经过验证的成对ITestDataSource;完整的笛卡尔积组合会改变测试数量和执行成本。 - 将
[Theory]、[Datapoint]和[DatapointSource]转换为显式的DynamicData或组合数据源。保留自动生成的 bool/enum 值,以及理论测试在所有行都违反假设时会失败这一规则。 [Explicit]、[Repeat]、参数化 fixture、fixture 数据源和Assert.Multiple都没有行为完全一致的机械映射。应谨慎重写或报告需人工跟进;切勿默默采用近似映射。- NUnit 的
[Retry(n)]将初始尝试计入次数,而 MSTest 的[Retry(n)]统计初始尝试之后的重试次数。应减去 1,并单独审查 NUnit 的RetryExceptions筛选器。 [CancelAfter(ms)]应映射为[Timeout(ms, CooperativeCancellation = true)]。在 VSTest 下,移除 NUnit 注入的CancellationToken参数,并使用注入的或基于属性的TestContext.CancellationToken;否则,VSTest 会将其视为缺少数据。仅当已证明保留的运行器支持注入时,才保留方法令牌。将 fixture 级默认值扩展到每个受影响的测试和生命周期方法。[Apartment(ApartmentState.STA)]应映射为[STATestClass]或[STATestMethod]。如果异步延续必须保留在 STA 线程上,请设置UseSTASynchronizationContext = true。MTA 是 MSTest 的默认值。[RequiresThread]和[SingleThreaded]保证线程身份,而 MSTest 属性通常无法保留这一点;应使用自定义执行器或报告需人工跟进。- 将 NUnit 的平台和区域性限制映射为
OSCondition、ArchitectureCondition或MemberCondition帮助程序。将SetCulture和SetUICulture映射为保存并恢复原始区域性的 setup/cleanup。 - 将
[DefaultFloatingPointTolerance]展开为每个受影响断言的显式 delta。保留方法优先于 fixture、fixture 优先于程序集的优先级。
5. 保留生命周期和命名空间设置
[SetUp]和[TearDown]仍是每个测试的生命周期方法。- 对于 NUnit 默认的单个夹具实例,请审查所有可变实例字段。如果测试依赖共享状态,请使用
[ClassInitialize]创建静态状态,并通过[ClassCleanup]释放。 [FixtureLifeCycle(LifeCycle.InstancePerTestCase)]已与 MSTest 的类实例化方式一致;移除该特性并保留每个测试的实例状态。- NUnit 的
[SetUpFixture]作用于命名空间范围。MSTest 的程序集初始化则作用于整个程序集。只有当 NUnit 设置原本就覆盖整个程序集时才使用[AssemblyInitialize];否则,将设置移至受影响的类或共享辅助程序中,不要扩大作用范围。 - 保留设置和清理的继承顺序。除非能证明结果顺序等效,否则不要合并基类和派生类的方法。
- 将 NUnit 的
TestContext.WriteLine映射为通过注入或属性获取的 MSTestTestContext.WriteLine。分别转换目录、测试名称和附件相关 API。
6. 保留并行化和排序
NUnit 和 MSTest 默认都按串行方式运行。对于未配置并行化的 NUnit 项目,不要添加并行化。
当 NUnit 明确启用并行化时:
- 程序集级别的
[Parallelizable(ParallelScope.Fixtures)]->[assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)] - 夹具级别的
[Parallelizable(ParallelScope.Children)]或方法级并行化 -> 仅在确认共享实例状态安全后,才使用 MSTest 方法级并行化 - 启用并行化时,
[NonParallelizable]->[DoNotParallelize] [LevelOfParallelism(N)]->Workers = N- 如果能保留比
[DoNotParallelize]更窄的锁定范围,可使用 MSTest 4.4 的[ResourceLock("key")]实现针对资源的串行化
NUnit 的 [Order] 并不表示通用的依赖关系语义。只有当源代码确实表达前置条件时,才使用 MSTest 4.4 的 [DependsOn];否则,通过让测试相互独立来移除排序,或报告为需手动跟进。
7. 验证一致性
- 使用与基线相同的平台、筛选器和配置运行测试。
- 比较发现、通过、失败和跳过的数量。
- 调查每一处差异:
- 缺失的行 ->
DataRow、DynamicData、组合式数据或行元数据转换 - 异常或类型发生变化 -> 精确类型与派生类型映射
- 状态失败 -> NUnit 单实例夹具语义或设置顺序
- 并发失败 ->
Parallelize、DoNotParallelize、工作线程数或资源锁 - 跳过情况发生变化 ->
Ignore、Explicit、行级忽略或重试行为
- 缺失的行 ->
- 确认没有遗留 NUnit 包、命名空间、特性、约束、适配器设置或自定义 NUnit 扩展,除非已记录为需后续跟进。
- 回读高风险变更文件,并在结果中列出确切的目标 API。
使用以下最终回复格式:
- 已更改: 文件及确切的高风险映射。
- 已验证: 最终命令,以及发现、通过、失败和跳过的数量。
- 已保留: 目标框架、测试平台、夹具范围和并发选择。
- 待处理: 需手动跟进的事项,或无。
完成标准
- 已确认 NUnit 版本和测试平台
- 已转换 NUnit 包和源代码结构
- 目标框架和测试平台保持不变
- 已审查并在可观察到的范围内保留默认单实例夹具语义
- 已保留数据行元数据和断言语义
- 未在未说明的情况下扩大命名空间设置和并行化范围
- 已将 NUnit 官方特性索引中的每个特性归类为直接转换、重写、移除或手动处理
- 构建成功,测试结果与基线一致
- 已说明不受支持的自定义扩展或行为
后续操作
如果用户也需要 MTP,请单独运行 migrate-vstest-to-mtp。只有在一致性得到确认后,才使用 writing-mstest-tests。