关于 migrate-xunit-to-mstest
将 xUnit.net v2 或 v3 测试项目迁移到 MSTest v4,同时保留目标框架和测试平台的技能。重点处理语义一致性、运行器配置及构建和测试验证。
主要功能
- 01xUnit 属性与断言映射到 MSTest
- 02保留 VSTest 或 MTP 运行器
- 03映射夹具生命周期与并行设置
- 04检查目标框架兼容性
- 05构建与测试验证
适用场景
- 01将 xUnit 测试项目迁移到 MSTest
- 02移植夹具和测试生命周期行为
- 03处理断言及运行器差异
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill migrate-xunit-to-mstest安装后在 Claude Code 中输入/migrate-xunit-to-mstest确认已加载。
附带文件references/mapping-cheatsheet.md
详细介绍
xUnit -> MSTest 迁移
将 xUnit.net v2 或 v3 测试转换为 MSTest v4,且不更改目标框架或测试平台。成功迁移后,项目可以构建、发现相同的测试,并保留通过/失败结果和执行语义。
范围
仅当项目包含 xUnit 包或源代码,且用户希望使用 MSTest 时,才使用此技能。如果项目已使用 MSTest,且不包含 xUnit 测试,则说明无需迁移测试框架,并且不做任何更改。
不要将此框架转换与目标框架升级或 VSTest/MTP 迁移合并进行。完成并验证一项迁移后,再开始另一项。
工作区约定
- 激活技能后继续操作。在当前工作目录中搜索已暂存的项目和源代码;绝不要在此技能的基础目录下查找用户文件。
- 打开 glob/搜索返回的原始路径。如果读取或补丁工具拒绝刚找到的路径,请改用其他可用工具重试。只有在穷尽当前工作区的搜索后,才能向用户询问路径。
- 根据用户要求的交付内容进行判断:“转换此项目”表示需要编辑、构建并测试;“给我一个计划”或“我该如何转换?”表示需要作答。如果文件已存在,不要用“请提供文件”来代替实际操作。
- 最终回复必须说明源 xUnit 版本、保留的运行器、已更改的文件、应用的每项高风险语义映射,以及实际测试数量。关于 fixture 生命周期、Owner 映射、取消或并行化的说明,必须体现在最终生成的源代码中,不能只写在说明文字里。
响应模式
- **完整迁移请求:**检查项目、进行修改、构建并运行测试。不要只提供计划。
- **针对特定编译错误或 API 的问题:**检查相关代码,并仅应用对应的映射。不要叙述完整工作流程。
- **不支持的目标框架:**在更改包之前停止。MSTest v4 要求测试应用使用 .NET 8+ 或 .NET Framework 4.6.2+;可提议经单独批准升级 TFM,或将 MSTest v3 作为中间目标。
会影响结果的决策
在机械式映射之前,先执行以下决策:
| 检测到的状态 | 必须采取的操作 |
|---|---|
| 不再存在 xUnit 包、命名空间、特性或 fixture | 停止操作。不更改任何文件,说明无需迁移,并运行一次现有的 dotnet test 命令,以确认当前 MSTest 项目运行正常。 |
| 源项目使用 VSTest | 保留现有的 VSTest 属性/配置。优先保留并更新源项目显式指定的 Microsoft.NET.Test.Sdk 版本;如果仓库有意依赖 MSTest 汇总包的传递依赖,也可以沿用此约定。不要引入 MTP 属性。 |
| 源项目使用 MTP | 将 xUnit 专用的 MTP 选择替换为 MSTest MTP 配置。优先使用 MSTest.Sdk;若使用汇总包,则设置 EnableMSTestRunner=true 和 OutputType=Exe。保留原生与桥接命令集成方式,不要添加 <UseVSTest>true</UseVSTest> 或其他仅适用于 VSTest 的配置。 |
| 源项目依赖 xUnit 的默认并行化 | 如果当前项目至少有两个可独立运行的测试类,则在一个已编译的 .cs 文件中添加 [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]。如果项目只有一个测试类且未显式设置并行方式,则省略此项,因为类级并发无法体现。无论当前测试类数量多少,都要转换显式的 CollectionBehavior 或 xunit.runner.json 设置。报告完成前,重新读取已更改的文件,并在结果中指出其名称。 |
如需了解详细映射和示例,请在 references/mapping-cheatsheet.md 中搜索项目实际包含的结构,并且只阅读对应章节。不要加载或复述整份参考文档。
快速路径
对于常规项目迁移,分四个阶段完成:一次批量发现读取/搜索、一次编辑、一条 dotnet test 命令,以及一份简洁结果。不要:
- 列出目录后,再通过其他工具重新读取相同文件
- 将项目文件复制到已加载的技能或其
references/目录中;这些文件是只读指南,不是编辑工作区 - 除非已确认还原状态是最新的,否则不要尝试
dotnet test --no-restore - 在
dotnet test已足够时,分别运行还原、生成和测试命令 - 重复运行已通过的测试命令,或检查未更改的文件进行确认
如有现成的 CI/测试结果,请将其用作一致性基准。只有在计数不可用,且迁移包含数据驱动测试、固定数据、跳过项、自定义扩展、共享状态或其他无法仅凭源代码确定一致性的行为时,才运行新的编辑前基准测试。
工作流
1. 确定基准
- 在一次发现过程中,批量读取测试项目以及
Directory.Build.props、Directory.Packages.props、global.json和运行器配置,并搜索源码中以下高风险构造。 - 说明检测到的源版本:
xunit2.x 及相关包 -> xUnit v2xunit.v3或xunit.v3.*-> xUnit v3
- 根据项目和仓库配置识别 VSTest 或 MTP。仅在平台不明确时使用
platform-detection,并保留检测到的平台。 - 记录目标框架;如果 MSTest v4 不支持这些框架,则停止。
- 如果快速路径要求建立新基准,则运行一次现有测试命令,并记录发现、通过、失败和跳过的数量。
- 编辑前清点高风险构造:
IClassFixture、ICollectionFixture、CollectionDefinition、自定义FactAttribute/TheoryAttribute/DataAttributeAssert.Throws、ThrowsAny、IsType、Record.Exception、事件断言ITestOutputHelper、TestContext.Current、IAsyncLifetimeCollectionBehavior、xunit.runner.json、共享静态状态或外部状态
2. 替换包,但不要切换运行器
从项目文件和中央包文件中移除 xUnit 包,包括 xunit*、xunit.v3.*、xunit.runner.visualstudio、YTest.MTP.XUnit2,以及将被替换的 xUnit 专用配套包。
对于增量转换,默认使用 MSTest v4 元包:
<PackageReference Include="MSTest" Version="4.1.0" />
此元包包含 Microsoft.NET.Test.Sdk、MSTest.TestAdapter、MSTest.TestFramework 和 MSTest.Analyzers。保留 VSTest 时,优先保留源项目中显式指定的 Microsoft.NET.Test.Sdk 版本,并将其更新为与所选 MSTest 版本兼容的版本;这样更便于审查运行器和版本兼容性。有意依赖元包传递依赖项的仓库也可以保留这一惯例。对于上述示例中的版本,MSTest 4.1.0 要求 Microsoft.NET.Test.Sdk 18.0.1 或更高版本;不兼容的旧版本可能导致 NU1605。
保留 MTP 时,不要将 xUnit 的 UseMicrosoftTestingPlatformRunner 属性带入 MSTest 项目。优先使用已解析版本的 MSTest.Sdk。如果仓库惯例要求使用元包方案,则设置 <EnableMSTestRunner>true</EnableMSTestRunner> 和 <OutputType>Exe</OutputType>。只有在仓库继续通过 VSTest 命令模式调用 MTP 应用时,才保留 TestingPlatformDotnetTestSupport=true;原生 .NET 10 及更高版本的 MTP 模式不需要此设置。使用 MSTest.Sdk 保留 VSTest 时,设置 <UseVSTest>true</UseVSTest>。
不要更改 TargetFramework。只有移植了 xunit.runner.json 中相关设置后,才移除该文件。
3. 执行机械转换
先应用常见的改写:
| xUnit | MSTest |
|---|---|
| 无类特性 | [TestClass] |
[Fact] | [TestMethod] |
[Theory] + [InlineData] | [TestMethod] + [DataRow] |
[MemberData] | [DynamicData] |
[Fact(Skip = "...")] | [TestMethod] + [Ignore("...")] |
[Trait("Category", value)] | [TestCategory(value)] |
[Trait("Owner", value)] | [Owner(value)] |
其他 [Trait(key, value)] | [TestProperty(key, value)] |
Assert.Equal / NotEqual | Assert.AreEqual / AreNotEqual |
Assert.True / False | Assert.IsTrue / IsFalse |
Assert.Null / NotNull | Assert.IsNull / IsNotNull |
移除 using Xunit; 和 using Xunit.Abstractions;。使用元包选项时,添加 using Microsoft.VisualStudio.TestTools.UnitTesting;;MSTest.Sdk 会隐式导入它。
保留现有的类继承关系。不要机械地将类设为 sealed。
4. 处理语义映射
为步骤 1 中发现的每种高风险结构加载映射速查表。以下规则必须遵守:
- xUnit 的
Assert.Throws<T>要求精确类型,应映射到 MSTest 的Assert.ThrowsExactly<T>。 - xUnit 的
Assert.ThrowsAny<T>允许派生类型,应映射到 MSTest 的Assert.Throws<T>。 - xUnit 的
Assert.IsType<T>要求精确类型,应映射到泛型Assert.IsExactInstanceOfType<T>;Assert.IsAssignableFrom<T>应映射到泛型Assert.IsInstanceOfType<T>。如果将 xUnit 断言的类型化返回值赋给变量,应保留该赋值,并使用 MSTest 的泛型重载,而非非泛型Type重载。 - xUnit 的
Assert.Equal用于序列时会比较元素。MSTest 4.3+ 应使用Assert.AreSequenceEqual;较早的 v4 应使用CollectionAssert.AreEqual并传入已物化的列表;绝不能将序列相等替换为基于引用的Assert.AreEqual。 [Ignore]和[Timeout]是修饰特性;保留[TestMethod],确保测试能被发现。[DataRow]的值必须与参数类型完全匹配。TestContext.Current.CancellationToken应映射为注入的 MSTestTestContext.CancellationToken;绝不能将其替换为CancellationToken.None或新的CancellationTokenSource。Owner是保留的 VSTest 属性。将[Trait("Owner", value)]映射为[Owner(value)],而非[TestProperty("Owner", value)]。- 对于没有 MSTest 对应项的断言(
Assert.Collection、Assert.All、Assert.Equivalent、Record.Exception、事件断言),必须明确手动改写。绝不能删除断言而不替换其验证逻辑。
当清单已明确所需映射时,应在一次编辑中完成机械改写和语义改写。默认不要运行中间构建;根据最终验证的编译错误,仅处理尚未解决的转换。
5. 保留生命周期、夹具作用域和并行化
- 有效时保留构造函数设置以及
IDisposable/IAsyncDisposable。将IAsyncLifetime映射为[TestInitialize]/[TestCleanup]。 IClassFixture<T>表示每个测试类有一个夹具实例,供该类中的所有方法共享。将其映射为一个静态T字段,由接受TestContext的静态[ClassInitialize]方法创建一次,并由静态[ClassCleanup]一次性释放。此映射绝不可使用[TestInitialize]/[TestCleanup],因为那会导致每个测试方法各有一个夹具。- 对于
ICollectionFixture<T>,同时保留共享和串行执行行为。优先使用由各成员类调用的静态Lazy<T>辅助程序;仅当源集合禁用了并行化时,才添加[DoNotParallelize]。只有当夹具确实是整个程序集范围时,才使用程序集初始化。 - 将
ITestOutputHelper替换为通过构造函数注入或基于属性的 MSTestTestContext,并将每个_output.WriteLine(...)调用替换为相应的TestContext.WriteLine(...)。最终结果必须明确写出TestContext的注入/属性方式以及WriteLine的映射;仅说“已迁移输出”不足以证明行为一致。
xUnit 默认并行运行各测试类;MSTest 则串行运行。当当前项目有两个或更多可独立运行的测试类时,使用以下方式保留这一有效行为:
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
对于没有显式 xUnit 并行设置的单类项目,不要添加程序集策略:无需保留类级并发。始终翻译显式的 CollectionBehavior 或 xunit.runner.json 设置。绝不可使用 ExecutionScope.MethodLevel 来模拟 xUnit。应用夹具作用域或并行化决策前,说明源项目共享或串行执行的内容,以及目标项目如何保留这些行为。
6. 验证行为一致性
- 使用基线测试所用的相同平台、筛选器和配置运行一次测试。
dotnet test默认会构建;仅当需要单独定位编译失败时,才单独运行构建。 - 比较已发现、通过、失败和跳过的测试数量。
- 在宣布完成前调查每一处差异:
- 缺失用例 -> 发现特性、
DynamicData或DataRow字面量类型 - 异常行为变化 -> 精确类型与派生类型断言的映射
- 共享状态失败或耗时大幅变化 -> 夹具作用域和并行化
- 测试被静默跳过 -> 缺少
[TestMethod]或运行时跳过转换不正确
- 缺失用例 -> 发现特性、
- 确认没有残留 xUnit 包、命名空间、特性、运行器配置或夹具接口,除非已明确记录为需手动跟进的事项。
- 最终测试通过后,重新检查每个实现高风险映射的变更文件,以及任何运行器或并行化配置。在结果中写明文件名,以及用于实现生命周期、数据、跳过、输出、断言、夹具作用域或并行化行为的具体目标 API。保留类型参数和成员名称,例如
[ClassInitialize]、TestContext.WriteLine和Assert.IsExactInstanceOfType<T>;“已转换特性”或“已迁移输出”这类笼统说法不能证明行为一致性。
最终回复保持简洁并聚焦结果:
- **已更改:**列出文件名及应用的具体高风险映射。
- **已验证:**提供最终测试命令,以及已发现/通过/失败/跳过的数量。
- **已保留:**说明未更改的目标框架和测试平台,以及任何夹具或并行化作用域决策。
- **待处理:**指出需手动跟进的事项,或说明没有。
完成标准
- 已确认当前的 xUnit 版本和测试平台
- 已转换 xUnit 包和源代码构造
- 目标框架和测试平台保持不变
- 明确说明了 fixture 作用域和实际并行化决策;如果无法观察到并发,则应说明有理据的省略原因
- 构建成功
- 测试发现结果和结果计数与基线一致
- 对于不受支持的自定义扩展点,应明确指出,而不是进行近似处理
后续步骤
如果用户还希望使用 MTP,请单独运行 migrate-vstest-to-mtp。只有在确认行为一致后,才使用 writing-mstest-tests 来润色转换后的 MSTest 代码。