关于 migrate-vstest-to-mtp
用于将 .NET 测试解决方案从 VSTest 迁移到 Microsoft.Testing.Platform 的技能,指导配置框架专用运行器、按 SDK 设置 dotnet test、转换命令参数并更新 CI。
主要功能
- 01评估测试框架与 SDK 模式
- 02配置框架专用的 MTP 运行器
- 03按 SDK 版本设置 dotnet test 集成
- 04转换 VSTest 参数和筛选器
- 05更新 CI 命令并验证结果
适用场景
- 01将解决方案中的测试项目迁移至 MTP
- 02根据仓库 SDK 调整 dotnet test
- 03替换 CI 中的 VSTest 专用命令
- 04转换筛选器、覆盖率或崩溃转储选项
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill migrate-vstest-to-mtp安装后在 Claude Code 中输入/migrate-vstest-to-mtp确认已加载。
详细介绍
VSTest -> Microsoft.Testing.Platform 迁移
将 .NET 测试解决方案从 VSTest 迁移到 Microsoft.Testing.Platform(MTP)。迁移完成后,解决方案中的所有测试项目都将通过 MTP 运行,dotnet test 可正常工作,CI/CD 管道也已更新。
首要操作
在搜索网络或凭记忆作答之前,先检查提供的项目、Directory.Build.props、global.json 和 CI 文件。先确认目标框架和 SDK 模式:.NET 9 及更早版本使用兼容性属性和 -- 分隔符;.NET 10 原生 MTP 使用 global.json,移除该属性,并且传递 MTP 参数时不使用分隔符。对于集中式属性,切勿在 Directory.Build.props 中使用 IsTestProject 作为条件;应使用该文件中已有的属性,例如 MSBuildProjectName。
重要:不要在同一解决方案或运行配置中混用基于 VSTest 和基于 MTP 的 .NET 测试项目——这是不受支持的场景。
适用场景
- 将任何受支持的测试框架从 VSTest 切换到 Microsoft.Testing.Platform
- 启用测试项目的
dotnet run/dotnet watch/ 直接执行 - 启用 Native AOT 或修剪后的测试执行
- 在 MTP 上用
dotnet test替换vstest.console.exe - 将 CI/CD 管道从 VSTest 任务更新为 .NET Core CLI 任务
- 将
dotnet test参数从 VSTest 语法更新为 MTP 语法
不适用场景
- 项目已通过 Microsoft.Testing.Platform 运行,且无需解决其他 MTP 行为差异(例如,发现的测试数量为 0 时返回退出代码 8)
- 在不同测试框架之间迁移(例如,从 MSTest 迁移到 xUnit.net)——这是完全不同的工作
- 项目构建 UWP 或打包的 WinUI 测试项目——MTP 目前尚不支持这些项目
- 解决方案混用了 .NET 和非 .NET 测试适配器(例如 JavaScript 或 C++ 适配器)——必须使用 VSTest
- 升级 MSTest 版本——请使用
migrate-mstest-v1v2-to-v3或migrate-mstest-v3-to-v4
输入
| 输入 | 必填 | 说明 |
|---|---|---|
| 项目或解决方案路径 | 否 | 包含测试项目的 .csproj、.sln 或 .slnx 入口点。请自行通过 glob 搜索发现;只有未找到任何文件或选择确实存在歧义时才询问 |
| 测试框架 | 否 | MSTest、NUnit、xUnit.net v2 或 xUnit.net v3。根据包引用自动检测 |
| .NET SDK 版本 | 否 | 决定 dotnet test 集成模式。优先使用仓库的 global.json 或明确指定的 CI SDK;仅当仓库未固定或未注明 SDK 版本时,才使用主机上的 dotnet --version |
| CI/CD 管道文件 | 否 | 调用 vstest.console 或 dotnet test 的管道定义文件路径 |
执行与回答约定
- 在当前工作目录中查找项目、props、
global.json和流水线文件。打开字面搜索结果;技能目录不是用户的代码库。激活技能后继续操作,不要询问可查找到的路径。 - 对于实现请求,编辑文件,然后验证生效的 MSBuild 属性、转换后的命令、测试数量和所需产物。对于问题,针对检测到的 SDK 和框架提供一条准确的命令/配置,而不是罗列多个近似选项。
- 保留所有构建参数,避免引入
--no-build、新的设置文件或无关的包升级。除非文件确实存在,否则绝不要保留--settings <file>。 - 对于 .NET 9 及更早版本,显示
--分隔符。对于 .NET 10 原生 MTP 模式,明确说明要将其移除。 - 如果有意抑制退出代码 8,请警告:宽泛抑制可能会掩盖因筛选器错误导致的意外空运行;应将其限定在已知的零测试项目/配置中。
- 对于退出代码 8 的问题,展示以下三种具体形式:
--ignore-exit-code 8、TestingPlatformCommandLineArguments和TESTINGPLATFORM_EXITCODE_IGNORE=8。 - 对于 xUnit v3 筛选器,提供可直接使用的
--filter-class/--filter-method/--filter-trait命令,并解释它们按 AND 逻辑组合。对于已解析的 xUnit 版本支持的情况,还要提供复杂表达式的--filter-query路径语法,并链接到 xUnit 查询筛选器文档。 - 翻译报告器、覆盖率或转储相关内容时,必须在回答中写明每个必需的包;仅提供看似正确的选项而未说明其所属扩展包,属于不完整回答。
- 最终结果必须说明框架运行器选择加入方式、代码库所选的 SDK 集成模式、准确的转换后命令、扩展包以及验证证据。
工作流
第 1 步:评估解决方案
- 识别每个测试项目使用的测试框架——请参阅
platform-detection技能中的包到框架映射。主要指标:- MSTest:引用
MSTest或MSTest.TestAdapter,或使用MSTest.Sdk(且<IsTestApplication>未设为false)。注意:MSTest.TestFramework单独使用时只是库依赖,不是测试项目。 - NUnit:引用
NUnit3TestAdapter - xUnit.net:引用
xunit和xunit.runner.visualstudio
- MSTest:引用
- 从
global.json或用户明确提供的上下文中确定代码库/CI 使用的 SDK。仅当两者都不存在时,才回退到dotnet --version;不得让 Agent 主机 SDK 悄然覆盖明确指定的 .NET 8/9 CI 目标。 - 检查解决方案或代码库根目录下是否存在
Directory.Build.props文件——为保持一致,所有 MTP 属性都应放在该文件中 - 检查 CI 脚本或流水线定义中是否使用
vstest.console.exe - 检查 CI 脚本中是否有 VSTest 专用的
dotnet test参数:--filter、--logger、--collect、--settings、--blame* - 运行
dotnet test,确定测试通过/失败数量基线
步骤 2:设置 Directory.Build.props
关键:尽可能在解决方案或仓库根目录的
Directory.Build.props中设置 MTP 运行器属性,而不是按项目设置。这样可以避免配置不一致,例如某些项目使用 VSTest,而其他项目使用 MTP(这是不受支持的场景)。 注意:MTP 还要求测试项目设置<OutputType>Exe</OutputType>。只有MSTest.Sdk会自动设置此项。对于其他所有配置(启用了EnableMSTestRunner的 MSTest NuGet 包、启用了EnableNUnitRunner的 NUnit、YTest.MTP.XUnit2),建议在Directory.Build.props中通过条件集中设置<OutputType>Exe</OutputType>,且条件仅针对测试项目。如果无法在Directory.Build.props中可靠地仅针对测试项目,那么按项目设置<OutputType>Exe</OutputType>是可接受的例外。在
Directory.Build.props中使用条件:不要使用Condition="'$(IsTestProject)' == 'true'"——IsTestProject由测试 SDK targets 在后续求值阶段设置,在导入Directory.Build.props时尚不可用。请使用较早可用的属性(例如MSBuildProjectName),根据命名约定指定测试项目。例如,如果所有测试项目都以.Tests结尾:<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))"> <OutputType>Exe</OutputType> </PropertyGroup>请调整条件(例如
.EndsWith('Tests')、.Contains('.Test')),以匹配仓库使用的测试项目命名约定。将所有适用的集中属性都放在同一个条件中:
OutputType、框架运行器启用属性,以及在 .NET 9 及更早版本中的TestingPlatformDotnetTestSupport。不要保留仍会影响生产项目的无条件运行器属性。
步骤 3:启用特定框架的 MTP 运行器
每个框架都有自己的启用属性。为保持一致,请在 Directory.Build.props 中添加这些属性。
MSTest
选项 A——MSTest NuGet 包(3.2.0+):
<PropertyGroup>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>
确保项目引用 MSTest 3.2.0 或更高版本。如果当前版本已是 3.2.0+,迁移到 MTP 无需升级 MSTest 版本。
选项 B——MSTest.Sdk:
使用 MSTest.Sdk 时,默认启用 MTP——无需设置 EnableMSTestRunner 或 OutputType Exe 属性(SDK 会自动设置两者)。唯一需要做的是:如果项目中有 <UseVSTest>true</UseVSTest>,请将其移除。该属性会强制项目使用 VSTest,而不是 MTP。
NUnit
需要 NUnit3TestAdapter 5.0.0 或更高版本。
- 将
NUnit3TestAdapter更新到 5.0.0+:
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
- 启用 NUnit 运行器:
<PropertyGroup>
<EnableNUnitRunner>true</EnableNUnitRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>
xUnit.net
添加对 YTest.MTP.XUnit2 的引用——此包为 xUnit.net v2 项目提供 MTP 支持,无需升级到 xunit.v3。你还必须将 OutputType 设置为 Exe:
<PackageReference Include="YTest.MTP.XUnit2" Version="0.4.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
注意:
YTest.MTP.XUnit2保留了 VSTest 的--filter语法,因此 xUnit.net v2 无需迁移筛选器。它还支持用于 runsettings 的--settings(仅限 xunit 特定配置)、xunit.runner.json、通过--report-trx生成 TRX 报告,以及--treenode-filter。
xUnit.net v3
xUnit.net v3(xunit.v3 包)内置了 MTP 支持。通过以下方式启用:
<PropertyGroup>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
重要:xUnit.net v3 在 MTP 模式下不支持 VSTest 的
--filter语法。你必须将筛选器转换为 xUnit.net v3 的原生筛选选项(参见步骤 5)。
步骤 4:配置 dotnet test 集成
dotnet test 集成取决于 .NET SDK 版本。
.NET 10 SDK 及更高版本(推荐)
在 global.json 中添加 test 部分,以使用原生 MTP 模式:
{
"sdk": {
"version": "10.0.100"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
在此模式下,dotnet test 参数会直接传递——例如,dotnet test --report-trx。
重要:
global.json不支持尾随逗号。请确保 JSON 严格符合语法。
.NET 9 SDK 及更早版本
使用 dotnet test 命令的 VSTest 模式运行 MTP 测试项目,方法是在 Directory.Build.props 中添加此属性:
<PropertyGroup>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>
重要:在此模式下,必须使用
--将dotnet test的构建参数与 MTP 参数分隔开。例如:dotnet test --no-build -- --list-tests。
步骤 5:更新 dotnet test 命令行参数
VSTest 专用参数必须转换为对应的 MTP 参数。构建相关参数(-c、-f、--no-build、--nologo、-v 等)保持不变。
| VSTest 参数 | MTP 对应参数 | 备注 |
|---|---|---|
--test-adapter-path | 不适用 | MTP 不使用外部适配器发现 |
--blame | 不适用 | |
--blame-crash | --crashdump | 需要 Microsoft.Testing.Extensions.CrashDump NuGet 包 |
--blame-crash-dump-type <TYPE> | --crashdump-type <TYPE> | 需要 CrashDump 扩展 |
--blame-hang | --hangdump | 需要 Microsoft.Testing.Extensions.HangDump NuGet 包 |
--blame-hang-dump-type <TYPE> | --hangdump-type <TYPE> | 需要 HangDump 扩展 |
--blame-hang-timeout <TIMESPAN> | --hangdump-timeout <TIMESPAN> | 需要 HangDump 扩展 |
--collect "Code Coverage;Format=cobertura" | --coverage --coverage-output-format cobertura | 扩展专用参数 |
-d|--diag <LOG_FILE> | --diagnostic | |
--filter <EXPRESSION> | --filter <EXPRESSION> | MSTest、NUnit 和 xUnit.net v2(使用 YTest.MTP.XUnit2)语法相同。xUnit.net v3 请参阅下文的筛选器迁移 |
-l|--logger trx | --report-trx | 需要 Microsoft.Testing.Extensions.TrxReport NuGet 包 |
--results-directory <DIR> | --results-directory <DIR> | 相同 |
-s|--settings <FILE> | --settings <FILE> | MSTest 和 NUnit 仍支持 .runsettings |
-t|--list-tests | --list-tests | 相同 |
-- <RunSettings args> | --test-parameter | 仅适用于 MSTest 和 NUnit |
筛选器迁移
MSTest、NUnit 和 xUnit.net v2(使用 YTest.MTP.XUnit2):VSTest 的 --filter 语法在 VSTest 和 MTP 中完全相同,无需更改。
xUnit.net v3(原生 MTP):xUnit.net v3 在 MTP 上不支持 VSTest 的 --filter 语法。必须将筛选器转换为 xUnit.net v3 的原生筛选选项。
xUnit.net v3 筛选标志
| 标志 | 说明 |
|---|---|
--filter-class "name" | 运行指定类中的所有测试。支持通配符(*)。 |
--filter-not-class "name" | 排除指定类中的所有测试 |
--filter-method "name" | 运行指定的测试方法 |
--filter-not-method "name" | 排除指定的测试方法 |
--filter-namespace "name" | 运行指定命名空间中的所有测试 |
--filter-not-namespace "name" | 排除指定命名空间中的所有测试 |
--filter-trait "name=value" | 运行具有匹配 trait 的测试 |
--filter-not-trait "name=value" | 排除具有匹配 trait 的测试 |
单个标志可以指定多个值:--filter-class Foo Bar。
VSTest → xUnit.net v3 筛选器转换表
VSTest --filter 语法 | xUnit.net v3 MTP 对应参数 | 备注 |
|---|---|---|
FullyQualifiedName~ClassName | --filter-class *ClassName* | 子字符串匹配需要使用通配符 |
FullyQualifiedName=Ns.Class.Method | --filter-method Ns.Class.Method | 按完全限定的方法名精确匹配 |
Name=MethodName | --filter-method *MethodName* | 子字符串匹配使用通配符 |
Category=Value(trait) | --filter-trait "Category=Value" | 按 trait 名称/值对筛选 |
| 复杂表达式 | --filter-query "expr" | 使用 xUnit.net 查询筛选语言(见下文) |
xUnit.net v3 查询筛选语言
对于复杂表达式,请使用带路径段语法的 --filter-query:
/<assemblyFilter>/<namespaceFilter>/<classFilter>/<methodFilter>[traitName=traitValue]
每个段分别匹配:程序集名称、命名空间、类名、方法名。任何段中使用 * 表示“匹配全部”。文档:https://xunit.net/docs/query-filter-language
如果 --filter-class、--filter-method 或 --filter-trait 标志与原始筛选条件直接对应,请优先使用它们。只有在使用 --list-tests 验证查询后,才使用 --filter-query;语法上被接受但筛选结果为零个测试的查询,不算成功转换。
转换示例
# VSTest
dotnet test --filter "FullyQualifiedName~IntegrationTests&Category=Smoke"
# xUnit.net v3 MTP -- using individual filters (AND behavior)
dotnet test -- --filter-class *IntegrationTests* --filter-trait "Category=Smoke"
# xUnit.net v3 MTP -- using query language (assembly/namespace/class/method[trait])
dotnet test -- --filter-query "/*/*/*IntegrationTests*/*[Category=Smoke]"
注意:同时使用
--filter-class和--filter-trait时,两个条件必须都匹配(AND 行为)。对于复杂表达式,请使用带路径段语法的--filter-query。完整参考请参阅 xUnit.net 查询筛选语言文档。
第 6 步:安装 MTP 扩展包(如有需要)
如果 CI 脚本使用 TRX 报告、崩溃转储、挂起转储或覆盖率,请添加相应的包。使用中央包管理时,项目引用如下:
<ItemGroup>
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
<PackageReference Include="Microsoft.Testing.Extensions.CrashDump" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
</ItemGroup>
在 Directory.Packages.props 中添加匹配的 PackageVersion 条目。不使用中央包管理时,请在这些引用中填写从已配置源解析出的确切版本。确保版本与所选 MTP 技术栈兼容;不要照搬迁移指南中的固定版本。
第 7 步:更新 CI/CD 流水线
Azure DevOps
如果使用 VSTest 任务(VSTest@3):将其替换为 .NET Core CLI 任务(DotNetCoreCLI@2):
# Before (VSTest task)
- task: VSTest@3
inputs:
testAssemblyVer2: '**/*Tests.dll'
runSettingsFile: 'test.runsettings'
# After (.NET Core CLI task)
- task: DotNetCoreCLI@2
displayName: Run tests
inputs:
command: 'test'
arguments: '--no-build --configuration Release'
如果已使用 DotNetCoreCLI@2:根据第 5 步的转换更新参数。请记住,.NET 9 及更早版本需要使用 -- 分隔符:
- task: DotNetCoreCLI@2
displayName: Run tests
inputs:
command: 'test'
arguments: '--no-build -- --report-trx --results-directory $(Agent.TempDirectory)'
GitHub Actions
在工作流文件中更新 dotnet test 调用,使用与第 5 步相同的参数转换。
替换 vstest.console.exe
如果有脚本直接调用 vstest.console.exe,请将其替换为 dotnet test。测试项目现在是可执行文件,也可以直接运行。
第 8 步:处理行为差异
零测试退出代码
发现零个测试时,VSTest 会静默成功。MTP 会返回 退出代码 8。可选方案:
- 运行测试时传入
--ignore-exit-code 8 - 在
Directory.Build.props中添加:
<PropertyGroup>
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
</PropertyGroup>
- 使用环境变量:
TESTINGPLATFORM_EXITCODE_IGNORE=8
第 9 步:移除仅供 VSTest 使用的包(可选)
迁移完成并验证后,移除仅供 VSTest 使用的包:
Microsoft.NET.Test.Sdk—— MTP 不需要此包(MSTest.Sdk v4 默认已省略它)xunit.runner.visualstudio—— 仅供 VSTest 发现 xUnit.net 测试时使用(使用YTest.MTP.XUnit2时不需要)NUnit3TestAdapter的 VSTest 专属功能 —— 仍需要此适配器,但仅用于 MTP 运行器
注意:如果过渡期间需要保持与 VSTest 的兼容性,请保留这些包。
第 10 步:验证
- 运行
dotnet build—— 确认没有错误 - 运行
dotnet test—— 确认所有测试均通过 - 将测试通过数和失败数与迁移前的基准进行比较
- 直接运行测试可执行文件(例如
./bin/Debug/net8.0/MyTests.exe)—— 确认其正常工作 - 验证 CI 流水线生成预期的测试结果工件(TRX 文件、代码覆盖率、崩溃转储)
- 测试 Visual Studio(17.14+)或 VS Code 中的 Test Explorer 是否能发现并运行测试
验证
- 所有测试项目均使用 MTP 运行器(不再保留仅限 VSTest 的配置)
-
dotnet build执行完成且无错误 -
dotnet test通过所有测试,且测试数量与迁移前基线一致 - 测试可执行文件可直接运行(例如:
./bin/Debug/net8.0/MyTests.exe) - CI 流水线生成预期的测试结果产物(TRX 文件、代码覆盖率、崩溃转储)
- Visual Studio 或 VS Code 中的测试资源管理器能够发现并运行测试
- CI 脚本中不再调用
vstest.console.exe - 所有非 MSTest.Sdk 测试项目均设置了
<OutputType>Exe</OutputType>
常见问题
| 问题 | 解决方案 |
|---|---|
| 在同一个解决方案中混用 VSTest 和 MTP 项目 | 一起迁移所有测试项目——不支持混合模式 |
在 .NET 9 及更早版本中,dotnet test 参数被忽略 | 使用 -- 分隔构建参数与 MTP 参数:dotnet test -- --report-trx |
| CI 中没有失败却返回退出代码 8 | MTP 在未运行任何测试时会失败;请使用 --ignore-exit-code 8 或修复测试发现问题 |
| MSTest.Sdk v4 与 vstest.console 不再兼容 | MSTest.Sdk v4 不再添加 Microsoft.NET.Test.Sdk——请显式添加它,或改用 dotnet test |
缺少 <OutputType>Exe</OutputType> | 除 MSTest.Sdk(会自动设置)外,所有配置都必须设置此项 |
在 Directory.Build.props 中使用 Condition="'$(IsTestProject)' == 'true'" | 评估 Directory.Build.props 时尚未定义 IsTestProject——请改用 $(MSBuildProjectName.EndsWith('.Tests'))(或类似的基于名称的检查) |
后续步骤
- 使用
run-tests在新的 MTP 平台上运行测试 - 使用
mtp-hot-reload在 MTP 上通过热重载迭代修复测试