关于 migrate-mstest-v3-to-v4
用于将 MSTest v3 项目迁移到 v4 的技能,涵盖包、源码、运行时和测试发现变更,并根据项目状态与需求提供指导。
主要功能
- 01检查 MSTest 版本与目标框架
- 02指导更新包至 v4
- 03将源码变更对应到代码修复
- 04涵盖运行时与测试发现问题
适用场景
- 01将 MSTest v3 项目迁移到 v4
- 02修复升级后的编译错误
- 03诊断测试发现或 CI 失败
- 04评估运行时行为变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill migrate-mstest-v3-to-v4安装后在 Claude Code 中输入/migrate-mstest-v3-to-v4确认已加载。
详细介绍
MSTest v3 → v4 迁移
将测试项目从 MSTest v3 迁移到 MSTest v4。迁移后的项目使用 MSTest v4,能够顺利构建、通过测试,并处理所有源代码不兼容变更和行为变更。MSTest v4 与 MSTest v3 不具备二进制兼容性——任何基于 v3 编译的库都必须针对 v4 重新编译。
首要操作
在搜索网络或凭记忆作答之前,先检查提供的项目和源代码。将请求归类为针对性源代码修复、运行时行为变更、CI 发现问题、兼容性问题或完整迁移,然后按照下方对应的行处理。顺利编译并不代表无需使用此技能:发现失败、TestContext 生命周期异常和测试历史变更都属于运行时迁移失败。
适用场景
- 将
MSTest.TestFramework、MSTest.TestAdapter或MSTest元包从 3.x 升级到 4.x - 将
MSTest.Sdk从 3.x 升级到 4.x - 更新到 MSTest v4 包后修复构建错误
- 解决升级到 MSTest v4 后测试执行行为发生的变更
- 更新自定义
TestMethodAttribute或ConditionBaseAttribute实现以适配 v4
不适用场景
- 项目已经使用 MSTest v4,且能够顺利构建——迁移已完成
- 从 MSTest v1 或 v2 升级——先使用
migrate-mstest-v1v2-to-v3,然后再返回此处 - 项目未使用 MSTest
- 在不同测试框架之间迁移(例如从 MSTest 迁移到 xUnit 或 NUnit)
输入
| 输入 | 必需 | 描述 |
|---|---|---|
| 项目或解决方案路径 | 否 | 包含 MSTest 测试项目的 .csproj、.sln 或 .slnx 入口点。请自行发现:遍历工作目录;仅当未找到或选项确实模糊不清时才询问 |
| 构建命令 | 否 | 构建方式(例如 dotnet build、仓库构建脚本)。如果未提供,请自动检测 |
| 测试命令 | 否 | 测试运行方式(例如 dotnet test)。如果未提供,请自动检测 |
会改变处理结果的判断
| 检测到的请求或状态 | 必需操作 |
|---|---|
| 当前工作区中提供了文件 | 在其中搜索,并打开实际返回的路径。技能目录不是项目目录。如果某个工具无法读取有效路径,请改用其他可用的读取器或编辑器重试;不要向用户询问自己能够发现的路径。 |
| 用户要求修改提供的文件:“修复我的项目/文件”“请更新此源代码”“进行这些更改”或“然后构建并运行” | 修改所有受影响的位置。针对实际使用的包版本运行范围最小且有意义的构建/测试命令;不能因为启用了技能就只提供建议。 |
| 用户询问“我应该预期什么?”“如何修复这些变更?”,寻求兼容性建议或要求制定计划 | 根据实际项目状态直接回答,即使源代码可见也是如此。针对单一问题的回答应聚焦于该问题;仅包含会影响决策的相邻风险。 |
| 完整迁移中目标框架不受支持 | 先更新目标框架(TFM),再更新 MSTest 包,最后修复源代码中的问题。不要将此顺序埋在发行说明清单中。 |
存在自定义 TestMethodAttribute 子类 | 将 ExecuteAsync、CallerInfo 传播、显示名称处理以及子类的重试/结果语义视为一组相互关联的迁移内容。修复实际类,而不是占位示例。 |
存在 MSTest.Sdk v4 源代码/API 错误(ManagedType、TestTimeout、Contains) | 给出准确的源代码替换方案,然后补充相邻的运行器警告:MTP 模式不再提供 Microsoft.NET.Test.Sdk;仅当仍需要 VSTest 发现时才添加该包。 |
MSTest.Sdk v4 与 vstest.console 同时使用 | 这是 v4 的变更:MTP 模式不再引入 Microsoft.NET.Test.Sdk。保留 MTP 并添加该包以支持过渡期间的 VSTest 发现,启用 UseVSTest,或将 CI 迁移到 dotnet test;请说明所选方案保留的是哪个运行器。 |
响应指南
- 始终先确认当前版本:在推荐任何迁移步骤前,明确说明在项目中检测到的当前 MSTest 版本(例如:“您的项目使用 MSTest v3(3.8.0)”)。这能确认您已阅读项目文件,并为迁移建议提供依据。
- 确认目标版本,不要想当然:用户询问“最新版本”时,查询项目已配置的包源,并选择执行时可用的最新稳定版 MSTest v4。未经检查,绝不能直接照搬此技能中的示例版本。所有 MSTest 包都必须使用同一个已确认的版本。
- 针对性修复请求(用户升级后遇到特定编译错误):只处理步骤 3 中相关的重大变更。只有在用户要求交付源代码变更时才修改代码;“这些问题该如何修复?”属于回答请求。务必提供使用用户实际类型和方法名的具体修复代码。如果测试夹具仍引用 v3,不要声称 v3 构建成功就能验证与 v4 的兼容性;应在用户要求时更新包,或说明验证范围。对于自定义
TestMethodAttribute子类,展示完整的修复后类,包括将 CallerInfo 传递给基类构造函数。提及可能及早发现此问题的相关分析器(例如,针对 ExpectedException 的 MSTEST0006)。如果项目使用MSTest.Sdk,还要说明 v4 MTP 模式不再提供Microsoft.NET.Test.Sdk,以及这是否会影响当前可见的运行器。不要逐步介绍完整迁移流程。 - “会有哪些变化”类问题(用户在升级前询问重大变更):展示步骤 3 快速查阅表中的所有重大变更,而不只是当前代码中可见的变更。每项都用一行概述修复方法。还要提及步骤 4 中的关键行为变更(尤其是 TestCase.Id 历史记录影响和 TreatDiscoveryWarningsAsErrors 默认值)。如果可以查看项目代码,指出哪些变更会直接影响项目。
- 完整迁移请求(用户要求完成迁移):遵循下方的完整工作流程。
- 行为或运行时问题报告(用户描述测试执行差异,但没有构建错误):将描述的症状对应到步骤 4 的行为变更表。提供针对症状的建议。提醒用户留意其他行为变更。除非用户同时遇到构建错误,否则不要逐项讲解源代码层面的重大变更。
- CI 或测试发现问题(升级后未发现测试、vstest.console 停止工作或 CI 管道失败):重点说明 4.5(MSTest.Sdk v4 默认 MTP 模式不再包含 Microsoft.NET.Test.Sdk——但 vstest.console 仍需要它)和 4.4(TreatDiscoveryWarningsAsErrors)。清楚解释根本原因,并提供全部三种处理方式:添加 Microsoft.NET.Test.Sdk,同时保留 MTP;设置
UseVSTest以切换项目运行器;或将 CI 切换为使用 MTP 的dotnet test。不要逐步介绍完整迁移流程。 - 解释类问题(用户询问“这是已知变更吗?”或“还有什么需要注意的?”):解释相关变更并提供建议。提及用户接下来可能遇到的相关变更。不要要求执行完整迁移流程。
- 结果证明:实现工作结束时,报告检测到的 v3 版本、已确认的 v4 版本、运行器选择、修改的文件以及实际的构建和测试数量。绝不要根据推断声称构建成功、兼容 VSTest、测试发现正常或测试通过。
工作流程
**提交策略:**除非用户要求,否则不要创建提交。确保包、源代码和行为方面的变更在差异中逻辑上分开,但务必完成并验证用户要求的迁移。
步骤 1:评估项目
- 检查
.csproj、Directory.Build.props或Directory.Packages.props中对MSTest、MSTest.TestFramework、MSTest.TestAdapter或MSTest.Sdk的包引用,确定当前 MSTest 版本。 - 确认项目使用的是 MSTest v3(3.x)。如果使用的是 v1 或 v2,请先使用
migrate-mstest-v1v2-to-v3。 - 检查目标框架 -- MSTest v4 不再支持 .NET Core 3.1 至 .NET 7。支持的目标框架包括:net8.0、net9.0、net462(.NET Framework 4.6.2+)、uap10.0.16299(UWP)、net9.0-windows10.0.17763.0(现代 UWP)和 net8.0-windows10.0.18362.0(WinUI)。
- 检查是否有自定义的
TestMethodAttribute子类 -- 这些类在 v4 中需要修改。 - 检查是否使用了
ExpectedExceptionAttribute-- 该特性已在 v4 中移除(自 v3 起已弃用,并有分析器 MSTEST0006 提示)。 - 检查是否使用了
Assert.ThrowsException(已弃用)-- 该方法在 v4 中已移除。 - 执行干净构建,记录现有错误和警告作为基线。
步骤 2:将包更新到 MSTest v4
首先从配置的包源获取最新稳定版 v4。请在元包、独立包、MSTest.Sdk 和中央包管理中统一固定为该确切版本。
- 对于
MSTest元包,将其PackageReference更新为获取到的确切版本。 - 对于独立包,将
MSTest.TestFramework和MSTest.TestAdapter更新为相同版本。 - 对于
MSTest.Sdk,将项目或global.json中固定的 SDK 版本更新为相同版本。
运行 dotnet restore,然后运行 dotnet build。收集所有错误,供步骤 3 处理。
步骤 3:解决源代码中断性变更
系统地处理编译错误。使用此速查表找出所有适用的变更,然后逐一修复:
| 代码中的错误 / 模式 | 中断性变更 | 修复方法 |
|---|---|---|
自定义 TestMethodAttribute 重写了 Execute | Execute 已移除 | 改为 ExecuteAsync,返回 Task<TestResult[]>(3.1) |
[TestMethod("name")] 或自定义特性的构造函数 | 已添加 CallerInfo 参数 | 使用命名参数 DisplayName = "name";在子类中传递 CallerInfo(3.2) |
ClassCleanupBehavior.EndOfClass | 枚举已移除 | 移除参数:只保留 [ClassCleanup](3.3) |
TestContext.Properties.Contains("key") | Properties 是 IDictionary<string, object> | 改为 ContainsKey("key")(3.4) |
[Timeout(TestTimeout.Infinite)] | TestTimeout 枚举已移除 | 替换为 [Timeout(int.MaxValue)](3.5) |
TestContext.ManagedType | 属性已移除 | 改用 FullyQualifiedTestClassName(3.6) |
Assert.AreEqual(a, b, "msg {0}", arg) | 消息和参数重载已移除 | 使用字符串插值:$"msg {arg}"(3.7) |
Assert.ThrowsException<T>(...) | 已重命名 | 替换为 Assert.ThrowsExactly<T>(...) 或 Assert.Throws<T>(...)(3.7) |
Assert.IsInstanceOfType<T>(obj, out var t) | 输出参数已移除 | 使用 var t = Assert.IsInstanceOfType<T>(obj)(3.7) |
[ExpectedException(typeof(T))] | 特性已移除 | 将断言移至测试主体中:Assert.ThrowsExactly<T>(() => ...)(3.8) |
| 项目目标为 net5.0、net6.0 或 net7.0 | TFM 已移除 | 改为 net8.0 或 net9.0(3.9) |
重要:开始修复前,请扫描整个项目,查找上述所有模式。同一个项目中往往会同时存在多项中断性变更。
3.1 TestMethodAttribute.Execute -> ExecuteAsync
如果有自定义的 TestMethodAttribute 子类重写了 Execute,请改为 ExecuteAsync。这样修改是因为 v3 的同步 Execute API 会在测试代码内部使用 async/await 时造成死锁 -- 同步包装器会阻塞线程,而异步操作需要该线程才能完成。
// Before (v3)
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
// custom logic
return result;
}
}
// After (v4) -- Option A: wrap synchronous logic with Task.FromResult
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// custom logic (synchronous)
return Task.FromResult(result);
}
}
// After (v4) -- Option B: make properly async
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// custom async logic
return await base.ExecuteAsync(testMethod);
}
}
如果重写方法中的逻辑完全同步,请使用 Task.FromResult。如果调用 base.ExecuteAsync 或其他异步方法,请使用 async/await。
3.2 TestMethodAttribute 调用方信息构造函数
TestMethodAttribute 现在在其构造函数中使用 [CallerFilePath] 和 [CallerLineNumber] 参数。
如果你继承了 TestMethodAttribute,请将调用方信息传递给基类:
public class MyTestMethodAttribute : TestMethodAttribute
{
public MyTestMethodAttribute(
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
}
}
如果子类有自己的显示名称构造函数,请勿将该字符串传递给 v4 基类构造函数。只传递调用方信息,并设置
DisplayName 属性:
public sealed class NamedTestMethodAttribute : TestMethodAttribute
{
public NamedTestMethodAttribute(
string displayName,
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
DisplayName = displayName;
}
}
如果你使用 [TestMethodAttribute("Custom display name")],请改用命名参数语法:
// Before (v3)
[TestMethodAttribute("Custom display name")]
// After (v4)
[TestMethodAttribute(DisplayName = "Custom display name")]
3.3 移除 ClassCleanupBehavior 枚举
ClassCleanupBehavior 枚举已移除。在 v3 中,此枚举用于控制类清理是在类结束时(EndOfClass)还是程序集结束时(EndOfAssembly)运行。在 v4 中,类清理始终在类结束时运行。请移除枚举参数:
// Before (v3)
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void ClassCleanup(TestContext testContext) { }
// After (v4)
[ClassCleanup]
public static void ClassCleanup(TestContext testContext) { }
如果你之前使用了 ClassCleanupBehavior.EndOfAssembly,请将该清理逻辑移至 [AssemblyCleanup] 方法。
3.4 TestContext.Properties 类型变更
TestContext.Properties 的类型已从 IDictionary 变更为 IDictionary<string, object>。请将所有 Contains 调用更新为 ContainsKey:
// Before (v3)
testContext.Properties.Contains("key");
// After (v4)
testContext.Properties.ContainsKey("key");
3.5 移除 TestTimeout 枚举
TestTimeout 枚举(仅包含 TestTimeout.Infinite)已移除。请替换为 int.MaxValue:
// Before (v3)
[Timeout(TestTimeout.Infinite)]
// After (v4)
[Timeout(int.MaxValue)]
3.6 移除 TestContext.ManagedType
TestContext.ManagedType 属性已移除。请改用 TestContext.FullyQualifiedTestClassName。
3.7 Assert API 签名变更
- 移除 message + params:以前同时接受
message和object[]参数的 Assert 方法,现在只接受message。请使用字符串插值,而不是格式字符串:
// Before (v3)
Assert.AreEqual(expected, actual, "Expected {0} but got {1}", expected, actual);
// After (v4)
Assert.AreEqual(expected, actual, $"Expected {expected} but got {actual}");
- 重命名 Assert.ThrowsException:
Assert.ThrowsExceptionAPI 已重命名。请使用Assert.ThrowsExactly(严格匹配类型)或Assert.Throws(接受派生异常类型):
// Before (v3)
Assert.ThrowsException<InvalidOperationException>(() => DoSomething());
// After (v4) -- exact type match (same behavior as old ThrowsException)
Assert.ThrowsExactly<InvalidOperationException>(() => DoSomething());
// After (v4) -- also catches derived exception types
Assert.Throws<InvalidOperationException>(() => DoSomething());
- 变更 Assert.IsInstanceOfType 的 out 参数:
Assert.IsInstanceOfType<T>(x, out var t)改为var t = Assert.IsInstanceOfType<T>(x):
// Before (v3)
Assert.IsInstanceOfType<MyType>(obj, out var typed);
// After (v4)
var typed = Assert.IsInstanceOfType<MyType>(obj);
请对每一处都应用此赋值改写,同时保留具体断言类型以及该类型化变量后续的所有用法。如果有源代码,请展示或编辑实际方法,而不是替换成泛型 MyType 示例,然后验证项目能够编译。
- 移除适用于 IEquatable<T> 的 Assert.AreEqual 重载:如果出现泛型类型推断错误,请显式将类型参数指定为
object。
3.8 移除 ExpectedExceptionAttribute
[ExpectedException] 属性已在 v4 中移除。在 MSTest 3.2 中,引入了 MSTEST0006 分析器,用于标记 [ExpectedException] 的用法,并建议在仍使用 v3 时迁移到 Assert.ThrowsExactly(这是非破坏性变更)。在 v4 中,该属性已完全移除。请迁移到 Assert.ThrowsExactly:
// Before (v3)
[ExpectedException(typeof(InvalidOperationException))]
[TestMethod]
public void TestMethod()
{
MyCall();
}
// After (v4)
[TestMethod]
public void TestMethod()
{
Assert.ThrowsExactly<InvalidOperationException>(() => MyCall());
}
如果抛出异常的调用前有测试初始化代码,请只将抛出异常的调用包装在 lambda 中——保持清晰的 Arrange/Act 分离:
// Before (v3)
[ExpectedException(typeof(ArgumentNullException))]
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
service.Validate(null); // throws here
}
// After (v4)
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
Assert.ThrowsExactly<ArgumentNullException>(() => service.Validate(null));
}
对于异步测试方法,请使用 Assert.ThrowsExactlyAsync:
// Before (v3)
[ExpectedException(typeof(HttpRequestException))]
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await client.GetAsync("https://localhost:0");
}
// After (v4)
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await Assert.ThrowsExactlyAsync<HttpRequestException>(
() => client.GetAsync("https://localhost:0"));
}
如果 [ExpectedException] 使用了 AllowDerivedTypes 属性,请改用 Assert.ThrowsAsync<T>(匹配基类类型),而不是 Assert.ThrowsExactlyAsync<T>(精确匹配类型)。
若要有针对性地迁移,请转换所提供源代码中所有带属性的方法,只包装预期会抛出异常的语句,将 arrange/初始化语句保留在 lambda 外,并运行受影响的测试。如果存在可编辑的项目文件,仅在说明文字中替换 API 并不完整。
3.9 移除的目标框架
MSTest v4 支持 .NET 8 及更高版本和 .NET Framework 4.6.2 及更高版本。平台专用的受支持目标还包括 uap10.0.16299(UWP);现代 UWP 和 WinUI 使用各自对应的、受支持的 Windows 专用 .NET TFM。已停止支持 .NET Core 3.1 至 .NET 7。
如果测试项目面向不受支持的框架,请更新 TargetFramework:
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
3.10 展开策略移至 TestMethodAttribute
UnfoldingStrategy 属性(在 MSTest 3.7 中引入)已从各个数据源属性(DataRowAttribute、DynamicDataAttribute)移至 TestMethodAttribute。
3.11 ConditionBaseAttribute.ShouldRun 重命名
ConditionBaseAttribute.ShouldRun 属性已重命名为 IsConditionMet。
3.12 内部化/移除的类型
之前公开的多个类型现在已改为内部类型或已移除:
MSTestDiscoverer、MSTestExecutor、AssemblyResolver、LogMessageListenerTestExecutionManager、TestMethodInfo、TestResultExtensionsUnitTestOutcomeExtensions、GenericParameterHelper- PlatformServices 程序集中的
ITestMethod(TestFramework 中的版本未更改)
如果你的代码引用了其中任何类型,请寻找替代方案或移除相关依赖。
步骤 4:处理行为变更
这些变更不会导致构建错误,但可能影响测试运行时行为。
| 症状 | 原因 | 修复方法 |
|---|---|---|
| Azure DevOps 中的测试显示为新测试/测试历史丢失 | TestCase.Id 的生成方式已更改(4.3) | 无需修改代码;历史记录将重新建立基线 |
在 [ClassInitialize] 中访问 TestContext.TestName 时抛出异常 | v4 强制执行生命周期范围规则(4.2) | 将访问移至 [TestInitialize] 或测试方法中 |
| 未发现测试/测试发现失败 | TreatDiscoveryWarningsAsErrors 现在默认为 true(4.4) | 修复警告,或在 .runsettings 中将其设为 false |
| 之前不会挂起的测试现在挂起 | AppDomain 默认已禁用(4.1) | 在 .runsettings 的 RunConfiguration 中将 DisableAppDomain 设为 false |
| v4 升级后,vstest.console 使用 MSTest.Sdk 时找不到测试 | MSTest.Sdk 默认使用 MTP;v4 在 MTP 模式下不再添加 Microsoft.NET.Test.Sdk(4.5) | 添加显式包并保留 MTP,设置 UseVSTest,或将 CI 切换为 dotnet test |
| 分析器产生新的警告 | 分析器严重级别已提升(4.6) | 修复警告,或在 .editorconfig 中抑制 |
4.1 DisableAppDomain 默认值为 true
AppDomain 默认处于禁用状态。在 .NET Framework 上,如果在 testhost 内运行(dotnet test 和 VS 的默认设置),MSTest 会自动重新启用 AppDomain。如果需要显式控制 AppDomain 隔离,请通过 .runsettings 设置:
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>
4.2 错误使用 TestContext 时会抛出异常
MSTest v4 在错误的生命周期阶段访问测试专属属性时会抛出异常:
TestContext.FullyQualifiedTestClassName-- 不能在[AssemblyInitialize]中访问TestContext.TestName-- 不能在[AssemblyInitialize]或[ClassInitialize]中访问
修复方法:将 [ClassInitialize] 中访问 TestContext.TestName 的代码移至 [TestInitialize] 或单个测试方法中,以便使用每个测试各自的上下文。不要用 FullyQualifiedTestClassName 替代 TestName 作为变通方法——两者的语义不同。
4.3 TestCase.Id 生成方式已更改
TestCase.Id 的生成算法已更改,以修复长期存在的问题。这可能会影响 Azure DevOps 测试结果跟踪(例如,随时间跟踪测试失败情况)。无需修改代码,但请注意测试结果历史记录会出现中断。
4.4 TreatDiscoveryWarningsAsErrors 默认值为 true
v4 使用更严格的默认设置。现在,测试发现警告会被视为错误,这意味着以前即使存在测试发现问题仍能运行的测试,现在可能会彻底失败。如果升级后出现意外的测试失败(不是构建错误,而是未发现测试),请检查是否存在测试发现警告。调查期间如需恢复 v3 的行为:
<RunSettings>
<MSTest>
<TreatDiscoveryWarningsAsErrors>false</TreatDiscoveryWarningsAsErrors>
</MSTest>
</RunSettings>
建议:修复根本的测试发现警告,而不是抑制此设置。
4.5 MSTest.Sdk 与 vstest.console 的兼容性
MSTest.Sdk 默认使用 Microsoft.Testing.Platform(MTP)模式。MSTest.Sdk v3 在该模式下仍会添加 Microsoft.NET.Test.Sdk;v4 则移除了这个不必要的引用。因此,CI 流水线如果单独调用 vstest.console,升级到 v4 后可能会立即发现 0 个测试。
选项 A —— 保留 MTP 并临时兼容 VSTest 测试发现:显式添加确切兼容的 Microsoft.NET.Test.Sdk 包。如果 MTP 仍是主要运行器,但暂时无法移除现有的 vstest.console 作业,这是影响最小的修复方法:
直接使用 PackageReference,并指定从已配置的软件源解析出的确切兼容版本。采用中央包管理时,在 Directory.Packages.props 中添加或更新 Microsoft.NET.Test.Sdk 的 PackageVersion,并让项目引用保持不带版本号。不要照搬固定的示例版本。
使用实际的 vstest.console 命令进行验证;dotnet test 的 MTP 运行通过,并不能证明 VSTest 能发现测试。
选项 B —— 将项目切换到 VSTest 模式:设置 UseVSTest 属性。此时 MSTest.Sdk 会添加 Microsoft.NET.Test.Sdk:
<PropertyGroup>
<UseVSTest>true</UseVSTest>
</PropertyGroup>
保留第 2 步中解析出的确切 MSTest.Sdk v4 固定版本;此选项更改的是运行器,而不是选定的 MSTest 版本或目标框架。
选项 C —— 将 CI 切换到 dotnet test:将 CI 流水线中对 vstest.console 的调用替换为 dotnet test。此方法原生支持 MTP,也是 MSTest.Sdk 项目的长期推荐方案。
不要说此行为在 v4 之前就已存在:在 MTP 模式下移除传递引用 Microsoft.NET.Test.Sdk,是 v4 的一项破坏性行为变更。
4.6 分析器严重性级别变更
多个分析器的默认严重性级别已从 Info 提升为 Warning:
- MSTEST0001、MSTEST0007、MSTEST0017、MSTEST0023、MSTEST0024、MSTEST0025
- MSTEST0030、MSTEST0031、MSTEST0032、MSTEST0035、MSTEST0037、MSTEST0045
检查并修复所有新增警告;如果这些警告是有意保留的,则在 .editorconfig 中抑制它们。
步骤 5:验证
- 运行
dotnet build—— 确认没有错误,并检查所有新增警告 - 运行
dotnet test—— 确认所有测试均通过 - 将测试结果(通过/失败数量)与迁移前的基准进行比较
- 如果使用 Azure DevOps 测试跟踪,请注意
TestCase.Id的变更可能会影响历史记录的连续性 - 检查是否有测试因测试发现规则变严格而被悄然遗漏
验证清单
- 所有 MSTest 包均已更新到 4.x
- 项目构建无错误
- 所有测试均通过
dotnet test - 自定义
TestMethodAttribute子类已针对ExecuteAsync和 CallerInfo 更新 -
ExpectedExceptionAttribute已替换为Assert.ThrowsExactly -
Assert.ThrowsException已替换为Assert.ThrowsExactly(或Assert.Throws) - 已移除
ClassCleanupBehavior枚举用法 -
TestContext.Properties.Contains已更新为ContainsKey - 所有目标框架均为 net8.0+、net9.0、net462+、uap10.0.16299 或 WinUI
- 已检查并处理行为变更
- 迁移期间没有丢失测试(比较测试数量)
相关技能
writing-mstest-tests—— 了解现代 MSTest v4 断言 API 和测试编写最佳实践run-tests—— 了解迁移后如何运行测试
常见问题
| 问题 | 解决方案 |
|---|---|
自定义 TestMethodAttribute 仍重写 Execute | 改为返回 Task<TestResult[]> 的 ExecuteAsync |
TestMethodAttribute("display name") 不再编译通过 | 使用 TestMethodAttribute(DisplayName = "display name") |
找不到 ClassCleanupBehavior 枚举 | 移除枚举参数;现在 [ClassCleanup] 总是在类结束时运行。若要在程序集结束时清理,请使用 [AssemblyCleanup] |
缺少 TestContext.Properties.Contains | 使用 ContainsKey——Properties 现在是 IDictionary<string, object> |
找不到 ExpectedException 特性 | 在测试主体中使用 Assert.ThrowsExactly<T>(() => ...) 替代 |
找不到 Assert.ThrowsException | 替换为 Assert.ThrowsExactly(对于派生类型则使用 Assert.Throws) |
Assert.AreEqual 使用格式字符串参数时失败 | 使用字符串插值:$"message {value}" |
| 之前不会挂起的测试现在挂起 | AppDomain 默认已禁用;在 testhost 中运行 .NET Fx 时会自动重新启用 |
| Azure DevOps 测试历史记录中断 | 这是预期行为——TestCase.Id 的生成方式已更改;无需修改代码,结果会重新建立基准 |
| 发现警告现在会导致运行失败 | TreatDiscoveryWarningsAsErrors 默认值为 true;请修复发现警告 |
| Net6.0/net7.0 目标无法编译 | 更新为 net8.0——MSTest v4 支持 net8.0、net9.0、net462、uap10.0.16299、现代 UWP 和 WinUI |