migrate-mstest-v3-to-v4
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMSTest v3 -> v4 Migration
MSTest v3 -> v4 迁移
Migrate a test project from MSTest v3 to MSTest v4. The outcome is a project using MSTest v4 that builds cleanly, passes tests, and accounts for every source-incompatible and behavioral change. MSTest v4 is not binary compatible with MSTest v3 -- any library compiled against v3 must be recompiled against v4.
将测试项目从 MSTest v3 迁移到 MSTest v4。最终目标是项目使用 MSTest v4 后可正常构建、测试全部通过,并且适配所有源码不兼容和行为变更。MSTest v4 与 MSTest v3 不具备二进制兼容性——任何基于 v3 编译的库都必须针对 v4 重新编译。
When to Use
适用场景
- Upgrading ,
MSTest.TestFramework, orMSTest.TestAdaptermetapackage from 3.x to 4.xMSTest - Upgrading from 3.x to 4.x
MSTest.Sdk - Fixing build errors after updating to MSTest v4 packages
- Resolving behavioral changes in test execution after upgrading to MSTest v4
- Updating custom or
TestMethodAttributeimplementations for v4ConditionBaseAttribute
- 将 、
MSTest.TestFramework或 MSTest 元包从 3.x 升级到 4.xMSTest.TestAdapter - 将 从 3.x 升级到 4.x
MSTest.Sdk - 修复升级到 MSTest v4 包后出现的构建错误
- 解决升级到 MSTest v4 后测试执行过程中的行为变更
- 为 v4 版本更新自定义 或
TestMethodAttribute实现ConditionBaseAttribute
When Not to Use
不适用场景
- The project already uses MSTest v4 and builds cleanly -- migration is done
- Upgrading from MSTest v1 or v2 -- use first, then return here
migrate-mstest-v1v2-to-v3 - The project does not use MSTest
- Migrating between test frameworks (e.g., MSTest to xUnit or NUnit)
- 项目已经在使用 MSTest v4 且可正常构建——迁移已完成
- 从 MSTest v1 或 v2 升级——请先使用 ,再参照本文档操作
migrate-mstest-v1v2-to-v3 - 项目未使用 MSTest
- 不同测试框架之间的迁移(例如从 MSTest 迁移到 xUnit 或 NUnit)
Inputs
输入参数
| Input | Required | Description |
|---|---|---|
| Project or solution path | Yes | The |
| Build command | No | How to build (e.g., |
| Test command | No | How to run tests (e.g., |
| 输入 | 必填 | 描述 |
|---|---|---|
| 项目或解决方案路径 | 是 | 包含 MSTest 测试项目的 |
| 构建命令 | 否 | 构建方式(例如 |
| 测试命令 | 否 | 测试执行方式(例如 |
Response Guidelines
响应指南
- Always identify the current version first: Before recommending any migration steps, explicitly state the current MSTest version detected in the project (e.g., "Your project uses MSTest v3 (3.8.0)"). This confirms you've read the project files and grounds the migration advice.
- Focused fix requests (user has specific compilation errors after upgrading): Address only the relevant breaking changes from Step 3. Always provide concrete fixed code using the user's actual types and method names — show a complete, copy-pasteable code snippet, not just a description of what to change. For custom subclasses, show the full fixed class including CallerInfo propagation to the base constructor. Mention any related analyzer that could have caught this earlier (e.g., MSTEST0006 for ExpectedException). Do not walk through the entire migration workflow.
TestMethodAttribute - "What to expect" questions (user asks about breaking changes before upgrading): Present ALL major breaking changes from the Step 3 quick-lookup table -- not just the ones visible in the current code. For each, provide a one-line fix summary. Also mention key behavioral changes from Step 4 (especially TestCase.Id history impact and TreatDiscoveryWarningsAsErrors default). If project code is available, highlight which changes apply directly.
- Full migration requests (user wants complete migration): Follow the complete workflow below.
- Behavioral/runtime symptom reports (user describes test execution differences without build errors): Match described symptoms to the behavioral changes table in Step 4. Provide targeted, symptom-specific advice. Mention other behavioral changes the user should watch for. Do not walk through source breaking changes unless the user also has build errors.
- CI/test-discovery issues (tests not discovered, vstest.console stopped working, CI pipeline failures after upgrading): Focus on 4.5 (MSTest.Sdk defaults to MTP mode, which does not include Microsoft.NET.Test.Sdk -- needed for vstest.console) and 4.4 (TreatDiscoveryWarningsAsErrors). Explain the root cause clearly and give both fix options (add Microsoft.NET.Test.Sdk package or switch to ). Do not walk through the full migration workflow.
dotnet test - Explanatory questions (user asks "is this a known change?", "what else should I watch out for?"): Explain the relevant changes and advise. Mention related changes the user might encounter next. Do not prescribe a full migration procedure.
- 始终先识别当前版本:在推荐任何迁移步骤前,明确说明项目中检测到的当前 MSTest 版本(例如「你的项目使用 MSTest v3 (3.8.0)」)。这可以确认你已读取项目文件,且迁移建议有明确依据。
- 针对性修复请求(用户升级后遇到特定编译错误):仅处理步骤3中相关的破坏性变更。始终基于用户实际的类型和方法名提供具体的修复代码——展示完整可直接复制粘贴的代码片段,而不仅仅是修改说明。对于自定义 子类,展示完整的修复后类,包括向基构造函数传递 CallerInfo 的逻辑。提及可以提前发现此类问题的相关分析器(例如对应 ExpectedException 的 MSTEST0006)。无需遍历完整迁移流程。
TestMethodAttribute - 「预期影响」类问题(用户在升级前询问破坏性变更):展示步骤3快速查询表中的所有主要破坏性变更,而不仅仅是当前代码中可见的变更。为每个变更提供一行修复摘要。同时提及步骤4中的关键行为变更(尤其是 TestCase.Id 历史记录影响和 TreatDiscoveryWarningsAsErrors 默认值)。如果有项目代码,可高亮说明哪些变更会直接生效。
- 完整迁移请求(用户需要完整迁移):遵循下文的完整工作流程操作。
- 行为/运行时症状反馈(用户描述测试执行差异但无构建错误):将描述的症状与步骤4的行为变更表匹配,提供针对性的症状特定建议。提及用户需要留意的其他行为变更。除非用户同时有构建错误,否则无需讲解源码破坏性变更。
- CI/测试发现问题(测试未被发现、vstest.console 停止工作、升级后CI流水线失败):重点关注4.5(MSTest.Sdk 默认使用 MTP 模式,不包含 Microsoft.NET.Test.Sdk——而 vstest.console 需要该包)和4.4(TreatDiscoveryWarningsAsErrors)。清晰说明根因,同时给出两种修复方案(添加 Microsoft.NET.Test.Sdk 包或切换到 )。无需遍历完整迁移流程。
dotnet test - 解释类问题(用户询问「这是已知变更吗?」、「我还需要注意什么?」):解释相关变更并给出建议,提及用户后续可能遇到的相关变更。无需给出完整迁移流程。
Workflow
工作流程
Commit strategy: Commit at each logical boundary -- after updating packages (Step 2), after resolving source breaking changes (Step 3), after addressing behavioral changes (Step 4). This keeps each commit focused and reviewable.
提交策略:在每个逻辑边界提交代码——更新包后(步骤2)、解决源码破坏性变更后(步骤3)、处理行为变更后(步骤4)。这样可以保证每个提交内容聚焦、易于审核。
Step 1: Assess the project
步骤1:评估项目
- Identify the current MSTest version by checking package references for ,
MSTest,MSTest.TestFramework, orMSTest.TestAdapterinMSTest.Sdk,.csproj, orDirectory.Build.props.Directory.Packages.props - Confirm the project is on MSTest v3 (3.x). If on v1 or v2, use first.
migrate-mstest-v1v2-to-v3 - Check target framework(s) -- MSTest v4 drops support for .NET Core 3.1 through .NET 7. Supported target frameworks are: net8.0, net9.0, net462 (.NET Framework 4.6.2+), uap10.0.16299 (UWP), net9.0-windows10.0.17763.0 (modern UWP), and net8.0-windows10.0.18362.0 (WinUI).
- Check for custom subclasses -- these require changes in v4.
TestMethodAttribute - Check for usages of -- removed in v4 (deprecated since v3 with analyzer MSTEST0006).
ExpectedExceptionAttribute - Check for usages of (deprecated) -- removed in v4.
Assert.ThrowsException - Run a clean build to establish a baseline of existing errors/warnings.
- 检查 、
.csproj或Directory.Build.props中Directory.Packages.props、MSTest、MSTest.TestFramework或MSTest.TestAdapter的包引用,确认当前 MSTest 版本。MSTest.Sdk - 确认项目使用 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)。
- 检查是否有自定义 子类——v4 中这类代码需要修改。
TestMethodAttribute - 检查是否使用了 ——v4 中已移除(从 v3 开始已废弃,对应分析器 MSTEST0006)。
ExpectedExceptionAttribute - 检查是否使用了 (已废弃)——v4 中已移除。
Assert.ThrowsException - 执行清理构建,确认现有错误/警告基线。
Step 2: Update packages to MSTest v4
步骤2:将包更新到 MSTest v4
If using the MSTest metapackage:
xml
<PackageReference Include="MSTest" Version="4.1.0" />If using individual packages:
xml
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />If using MSTest.Sdk:
xml
<Project Sdk="MSTest.Sdk/4.1.0">Run , then . Collect all errors for Step 3.
dotnet restoredotnet build如果使用 MSTest 元包:
xml
<PackageReference Include="MSTest" Version="4.1.0" />如果使用独立包:
xml
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />如果使用 MSTest.Sdk:
xml
<Project Sdk="MSTest.Sdk/4.1.0">执行 ,然后执行 ,收集所有错误用于步骤3处理。
dotnet restoredotnet buildStep 3: Resolve source breaking changes
步骤3:解决源码破坏性变更
Work through compilation errors systematically. Use this quick-lookup table to identify all applicable changes, then apply each fix:
| Error / Pattern in code | Breaking change | Fix |
|---|---|---|
Custom | Execute removed | Change to |
| CallerInfo params added | Use |
| Enum removed | Remove argument: just |
| | Change to |
| | Replace with |
| Property removed | Use |
| Message+params overloads removed | Use string interpolation: |
| Renamed | Replace with |
| Out parameter removed | Use |
| Attribute removed | Move assertion into test body: |
| Project targets net5.0, net6.0, or net7.0 | TFM dropped | Change to net8.0 or net9.0 (3.9) |
Important: Scan the entire project for ALL patterns above before starting fixes. Multiple breaking changes often coexist in the same project.
系统性处理编译错误。使用下方快速查询表识别所有适用变更,然后逐一应用修复:
| 代码中的错误/模式 | 破坏性变更 | 修复方案 |
|---|---|---|
自定义 | Execute 已移除 | 改为返回 |
| 新增 CallerInfo 参数 | 使用 |
| 枚举已移除 | 移除参数:仅保留 |
| | 改为 |
| | 替换为 |
| 属性已移除 | 使用 |
| 移除了消息+参数的重载 | 使用字符串插值: |
| 已重命名 | 替换为 |
| 移除了 out 参数 | 改为 |
| 特性已移除 | 将断言移到测试体中: |
| 项目目标为 net5.0、net6.0 或 net7.0 | 不再支持该 TFM | 改为 net8.0 或 net9.0 (3.9) |
重要提示:开始修复前,请扫描整个项目查找上述所有模式。同一个项目中通常会同时存在多个破坏性变更。
3.1 TestMethodAttribute.Execute -> ExecuteAsync
3.1 TestMethodAttribute.Execute -> ExecuteAsync
If you have custom subclasses that override , change to . This change was made because the v3 synchronous API caused deadlocks when test code used / internally -- the synchronous wrapper would block the thread while the async operation needed that same thread to complete.
TestMethodAttributeExecuteExecuteAsyncExecuteasyncawaitcsharp
// 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);
}
}Use when your override logic is purely synchronous. Use / when you call or other async methods.
Task.FromResultasyncawaitbase.ExecuteAsync如果你的自定义 子类重写了 ,请改为 。该变更的原因是 v3 的同步 API 会在测试代码内部使用 / 时导致死锁——同步包装器会阻塞线程,而异步操作需要同一个线程才能完成。
TestMethodAttributeExecuteExecuteAsyncExecuteasyncawaitcsharp
// 改造前 (v3)
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
// 自定义逻辑
return result;
}
}
// 改造后 (v4) -- 方案A:用 Task.FromResult 包装同步逻辑
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// 自定义逻辑(同步)
return Task.FromResult(result);
}
}
// 改造后 (v4) -- 方案B:改为标准异步实现
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// 自定义异步逻辑
return await base.ExecuteAsync(testMethod);
}
}如果重写逻辑是纯同步的,使用 。如果调用了 或其他异步方法,使用 /。
Task.FromResultbase.ExecuteAsyncasyncawait3.2 TestMethodAttribute CallerInfo constructor
3.2 TestMethodAttribute CallerInfo 构造函数
TestMethodAttribute[CallerFilePath][CallerLineNumber]If you inherit from TestMethodAttribute, propagate caller info to the base class:
csharp
public class MyTestMethodAttribute : TestMethodAttribute
{
public MyTestMethodAttribute(
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
}
}If you use , switch to the named parameter syntax:
[TestMethodAttribute("Custom display name")]csharp
// Before (v3)
[TestMethodAttribute("Custom display name")]
// After (v4)
[TestMethodAttribute(DisplayName = "Custom display name")]TestMethodAttribute[CallerFilePath][CallerLineNumber]如果你继承了 TestMethodAttribute,请将调用方信息传递给基类:
csharp
public class MyTestMethodAttribute : TestMethodAttribute
{
public MyTestMethodAttribute(
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
}
}如果你使用了 ,请改为命名参数语法:
[TestMethodAttribute("自定义显示名称")]csharp
// 改造前 (v3)
[TestMethodAttribute("自定义显示名称")]
// 改造后 (v4)
[TestMethodAttribute(DisplayName = "自定义显示名称")]3.3 ClassCleanupBehavior enum removed
3.3 ClassCleanupBehavior 枚举已移除
The enum is removed. In v3, this enum controlled whether class cleanup ran at end of class () or end of assembly (). In v4, class cleanup always runs at end of class. Remove the enum argument:
ClassCleanupBehaviorEndOfClassEndOfAssemblycsharp
// Before (v3)
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void ClassCleanup(TestContext testContext) { }
// After (v4)
[ClassCleanup]
public static void ClassCleanup(TestContext testContext) { }If you previously used , move that cleanup logic to an method instead.
ClassCleanupBehavior.EndOfAssembly[AssemblyCleanup]ClassCleanupBehaviorEndOfClassEndOfAssemblycsharp
// 改造前 (v3)
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void ClassCleanup(TestContext testContext) { }
// 改造后 (v4)
[ClassCleanup]
public static void ClassCleanup(TestContext testContext) { }如果你之前使用了 ,请将对应的清理逻辑移到 方法中。
ClassCleanupBehavior.EndOfAssembly[AssemblyCleanup]3.4 TestContext.Properties type change
3.4 TestContext.Properties 类型变更
TestContext.PropertiesIDictionaryIDictionary<string, object>ContainsContainsKeycsharp
// Before (v3)
testContext.Properties.Contains("key");
// After (v4)
testContext.Properties.ContainsKey("key");TestContext.PropertiesIDictionaryIDictionary<string, object>ContainsContainsKeycsharp
// 改造前 (v3)
testContext.Properties.Contains("key");
// 改造后 (v4)
testContext.Properties.ContainsKey("key");3.5 TestTimeout enum removed
3.5 TestTimeout 枚举已移除
The enum (with only ) is removed. Replace with :
TestTimeoutTestTimeout.Infiniteint.MaxValuecsharp
// Before (v3)
[Timeout(TestTimeout.Infinite)]
// After (v4)
[Timeout(int.MaxValue)]仅包含 的 枚举已被移除。替换为 即可:
TestTimeout.InfiniteTestTimeoutint.MaxValuecsharp
// 改造前 (v3)
[Timeout(TestTimeout.Infinite)]
// 改造后 (v4)
[Timeout(int.MaxValue)]3.6 TestContext.ManagedType removed
3.6 TestContext.ManagedType 已移除
The property is removed. Use instead.
TestContext.ManagedTypeTestContext.FullyQualifiedTestClassNameTestContext.ManagedTypeTestContext.FullyQualifiedTestClassName3.7 Assert API signature changes
3.7 Assert API 签名变更
- Message + params removed: Assert methods that accepted both and
messageparameters now accept onlyobject[]. Use string interpolation instead of format strings:message
csharp
// 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 renamed: The APIs are renamed. Use
Assert.ThrowsException(strict type match) orAssert.ThrowsExactly(accepts derived exception types):Assert.Throws
csharp
// 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 parameter changed: changes to
Assert.IsInstanceOfType<T>(x, out var t):var t = Assert.IsInstanceOfType<T>(x)
csharp
// Before (v3)
Assert.IsInstanceOfType<MyType>(obj, out var typed);
// After (v4)
var typed = Assert.IsInstanceOfType<MyType>(obj);- Assert.AreEqual for IEquatable<T> removed: If you get generic type inference errors, explicitly specify the type argument as .
object
- 移除消息+参数重载:同时接受 和
message参数的 Assert 方法现在仅接受object[]。使用字符串插值替代格式化字符串:message
csharp
// 改造前 (v3)
Assert.AreEqual(expected, actual, "预期值为 {0},实际值为 {1}", expected, actual);
// 改造后 (v4)
Assert.AreEqual(expected, actual, $"预期值为 {expected},实际值为 {actual}");- Assert.ThrowsException 已重命名:API 已重命名。使用
Assert.ThrowsException(严格类型匹配)或Assert.ThrowsExactly(接受派生异常类型):Assert.Throws
csharp
// 改造前 (v3)
Assert.ThrowsException<InvalidOperationException>(() => DoSomething());
// 改造后 (v4) -- 严格类型匹配(与旧 ThrowsException 行为一致)
Assert.ThrowsExactly<InvalidOperationException>(() => DoSomething());
// 改造后 (v4) -- 同时捕获派生异常类型
Assert.Throws<InvalidOperationException>(() => DoSomething());- Assert.IsInstanceOfType out 参数变更:改为
Assert.IsInstanceOfType<T>(x, out var t):var t = Assert.IsInstanceOfType<T>(x)
csharp
// 改造前 (v3)
Assert.IsInstanceOfType<MyType>(obj, out var typed);
// 改造后 (v4)
var typed = Assert.IsInstanceOfType<MyType>(obj);- 移除了针对 IEquatable<T> 的 Assert.AreEqual 重载:如果遇到泛型类型推断错误,显式将类型参数指定为 。
object
3.8 ExpectedExceptionAttribute removed
3.8 ExpectedExceptionAttribute 已移除
The attribute is removed in v4. In MSTest 3.2, the analyzer was introduced to flag usage and suggest migrating to while still on v3 (a non-breaking change). In v4, the attribute is gone entirely. Migrate to :
[ExpectedException]MSTEST0006[ExpectedException]Assert.ThrowsExactlyAssert.ThrowsExactlycsharp
// Before (v3)
[ExpectedException(typeof(InvalidOperationException))]
[TestMethod]
public void TestMethod()
{
MyCall();
}
// After (v4)
[TestMethod]
public void TestMethod()
{
Assert.ThrowsExactly<InvalidOperationException>(() => MyCall());
}When the test has setup code before the throwing call, wrap only the throwing call in the lambda -- keep Arrange/Act separation clear:
csharp
// 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));
}For async test methods, use :
Assert.ThrowsExactlyAsynccsharp
// 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"));
}If used the property, use (base type matching) instead of (exact type matching).
[ExpectedException]AllowDerivedTypesAssert.ThrowsAsync<T>Assert.ThrowsExactlyAsync<T>[ExpectedException]MSTEST0006[ExpectedException]Assert.ThrowsExactlyAssert.ThrowsExactlycsharp
// 改造前 (v3)
[ExpectedException(typeof(InvalidOperationException))]
[TestMethod]
public void TestMethod()
{
MyCall();
}
// 改造后 (v4)
[TestMethod]
public void TestMethod()
{
Assert.ThrowsExactly<InvalidOperationException>(() => MyCall());
}如果测试在抛出异常的调用前有初始化代码,仅将抛出异常的调用包裹在 lambda 中——保持 准备/执行 逻辑的清晰分离:
csharp
// 改造前 (v3)
[ExpectedException(typeof(ArgumentNullException))]
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
service.Validate(null); // 此处抛出异常
}
// 改造后 (v4)
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
Assert.ThrowsExactly<ArgumentNullException>(() => service.Validate(null));
}对于异步测试方法,使用 :
Assert.ThrowsExactlyAsynccsharp
// 改造前 (v3)
[ExpectedException(typeof(HttpRequestException))]
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await client.GetAsync("https://localhost:0");
}
// 改造后 (v4)
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await Assert.ThrowsExactlyAsync<HttpRequestException>(
() => client.GetAsync("https://localhost:0"));
}如果 使用了 属性,请使用 (匹配基类型)替代 (严格类型匹配)。
[ExpectedException]AllowDerivedTypesAssert.ThrowsAsync<T>Assert.ThrowsExactlyAsync<T>3.9 Dropped target frameworks
3.9 不再支持的目标框架
MSTest v4 supports: net8.0, net9.0, net462 (.NET Framework 4.6.2+), uap10.0.16299 (UWP), net9.0-windows10.0.17763.0 (modern UWP), and net8.0-windows10.0.18362.0 (WinUI). All other frameworks are dropped -- including net5.0, net6.0, net7.0, and netcoreapp3.1.
If the test project targets an unsupported framework, update :
TargetFrameworkxml
<!-- Before -->
<TargetFramework>net6.0</TargetFramework>
<!-- After -->
<TargetFramework>net8.0</TargetFramework>MSTest v4 支持的框架包括: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)。所有其他框架都不再支持——包括 net5.0、net6.0、net7.0 和 netcoreapp3.1。
如果测试项目的目标框架不受支持,请更新 :
TargetFrameworkxml
<!-- 改造前 -->
<TargetFramework>net6.0</TargetFramework>
<!-- 改造后 -->
<TargetFramework>net8.0</TargetFramework>3.10 Unfolding strategy moved to TestMethodAttribute
3.10 展开策略移到 TestMethodAttribute
The property (introduced in MSTest 3.7) has moved from individual data source attributes (, ) to .
UnfoldingStrategyDataRowAttributeDynamicDataAttributeTestMethodAttributeMSTest 3.7 引入的 属性已从单个数据源特性(、)移到 。
UnfoldingStrategyDataRowAttributeDynamicDataAttributeTestMethodAttribute3.11 ConditionBaseAttribute.ShouldRun renamed
3.11 ConditionBaseAttribute.ShouldRun 已重命名
The property is renamed to .
ConditionBaseAttribute.ShouldRunIsConditionMetConditionBaseAttribute.ShouldRunIsConditionMet3.12 Internal/removed types
3.12 内部/已移除类型
Several types previously public are now internal or removed:
- ,
MSTestDiscoverer,MSTestExecutor,AssemblyResolverLogMessageListener - ,
TestExecutionManager,TestMethodInfoTestResultExtensions - ,
UnitTestOutcomeExtensionsGenericParameterHelper - in PlatformServices assembly (the one in TestFramework is unchanged)
ITestMethod
If your code references any of these, find alternative approaches or remove the dependency.
多个之前公开的类型现在已改为内部类型或被移除:
- 、
MSTestDiscoverer、MSTestExecutor、AssemblyResolverLogMessageListener - 、
TestExecutionManager、TestMethodInfoTestResultExtensions - 、
UnitTestOutcomeExtensionsGenericParameterHelper - PlatformServices 程序集中的 (TestFramework 中的对应类型无变化)
ITestMethod
如果你的代码引用了上述任何类型,请寻找替代方案或移除依赖。
Step 4: Address behavioral changes
步骤4:处理行为变更
These changes won't cause build errors but may affect test runtime behavior.
| Symptom | Cause | Fix |
|---|---|---|
| Tests show as new in Azure DevOps / test history lost | | No code fix; history will re-baseline |
| v4 enforces lifecycle scope (4.2) | Move access to |
| Tests not discovered / discovery failures | | Fix warnings, or set to false in .runsettings |
| Tests hang that didn't before | AppDomain disabled by default (4.1) | Set |
| vstest.console can't find tests with MSTest.Sdk | MSTest.Sdk defaults to MTP; | Add explicit package reference or switch to |
| New warnings from analyzers | Analyzer severities upgraded (4.6) | Fix warnings or suppress in .editorconfig |
这些变更不会导致构建错误,但可能影响测试运行时行为。
| 症状 | 原因 | 修复方案 |
|---|---|---|
| Azure DevOps 中测试显示为新用例 / 测试历史丢失 | | 无需代码修复;历史记录会重新建立基线 |
| v4 强制生命周期范围限制 (4.2) | 将访问逻辑移到 |
| 测试未被发现 / 发现失败 | | 修复警告,或在 .runsettings 中设为 false |
| 之前正常的测试现在卡住 | AppDomain 默认禁用 (4.1) | 在 .runsettings 的 |
| 使用 MSTest.Sdk 时 vstest.console 找不到测试 | MSTest.Sdk 默认使用 MTP;仅在 VSTest 模式下才会添加 | 显式添加包引用,或切换到 |
| 分析器抛出新警告 | 分析器严重级别提升 (4.6) | 修复警告,或在 .editorconfig 中抑制 |
4.1 DisableAppDomain defaults to true
4.1 DisableAppDomain 默认为 true
AppDomains are disabled by default. On .NET Framework, when running inside testhost (the default for and VS), MSTest re-enables AppDomains automatically. If you need to explicitly control AppDomain isolation, set it via :
dotnet test.runsettingsxml
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>AppDomain 默认已禁用。在 .NET Framework 中,当运行在 testhost 中时( 和 VS 的默认模式),MSTest 会自动重新启用 AppDomain。如果你需要显式控制 AppDomain 隔离,可以通过 设置:
dotnet test.runsettingsxml
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>4.2 TestContext throws when used incorrectly
4.2 TestContext 不当使用时抛出异常
MSTest v4 now throws when accessing test-specific properties in the wrong lifecycle stage:
- -- cannot be accessed in
TestContext.FullyQualifiedTestClassName[AssemblyInitialize] - -- cannot be accessed in
TestContext.TestNameor[AssemblyInitialize][ClassInitialize]
Fix: Move any code that accesses from to or individual test methods, where per-test context is available. Do not replace with as a workaround -- they have different semantics.
TestContext.TestName[ClassInitialize][TestInitialize]TestNameFullyQualifiedTestClassNameMSTest v4 现在会在错误的生命周期阶段访问测试专属属性时抛出异常:
- ——不能在
TestContext.FullyQualifiedTestClassName中访问[AssemblyInitialize] - ——不能在
TestContext.TestName或[AssemblyInitialize]中访问[ClassInitialize]
修复方案:将所有访问 的代码从 移到 或单个测试方法中,这些阶段可以获取到每个测试的上下文。不要用 替代 作为临时方案——二者语义不同。
TestContext.TestName[ClassInitialize][TestInitialize]FullyQualifiedTestClassNameTestName4.3 TestCase.Id generation changed
4.3 TestCase.Id 生成规则变更
The generation algorithm for has changed to fix long-standing bugs. This may affect Azure DevOps test result tracking (e.g., test failure tracking over time). There is no code fix needed, but be aware of test result history discontinuity.
TestCase.IdTestCase.Id4.4 TreatDiscoveryWarningsAsErrors defaults to true
4.4 TreatDiscoveryWarningsAsErrors 默认为 true
v4 uses stricter defaults. Discovery warnings are now treated as errors, which means tests that previously ran despite discovery issues may now fail entirely. If you see unexpected test failures after upgrading (not build errors, but tests not being discovered), check for discovery warnings. To restore v3 behavior while you investigate:
xml
<RunSettings>
<MSTest>
<TreatDiscoveryWarningsAsErrors>false</TreatDiscoveryWarningsAsErrors>
</MSTest>
</RunSettings>Recommended: Fix the underlying discovery warnings rather than suppressing this setting.
v4 使用了更严格的默认规则。发现警告现在会被视为错误,这意味着之前即使有发现问题仍能运行的测试现在可能完全失败。如果升级后遇到意外的测试失败(不是构建错误,而是测试未被发现),请检查发现警告。如果需要在排查问题期间恢复 v3 的行为:
xml
<RunSettings>
<MSTest>
<TreatDiscoveryWarningsAsErrors>false</TreatDiscoveryWarningsAsErrors>
</MSTest>
</RunSettings>推荐方案:修复底层的发现警告,而不是抑制该设置。
4.5 MSTest.Sdk and vstest.console compatibility
4.5 MSTest.Sdk 和 vstest.console 兼容性
MSTest.Sdk defaults to Microsoft.Testing.Platform (MTP) mode. In MTP mode, MSTest.Sdk does not add a reference to -- it only adds it in VSTest mode. This is not a v4-specific change; it applies to MSTest.Sdk v3 as well. Without , cannot discover or run tests and will silently find zero tests. This commonly surfaces during migration when a CI pipeline uses but the project uses MSTest.Sdk in its default MTP mode.
Microsoft.NET.Test.SdkMicrosoft.NET.Test.Sdkvstest.consolevstest.consoleOption A -- Switch to VSTest mode: Set the property. MSTest.Sdk will then automatically add :
UseVSTestMicrosoft.NET.Test.Sdkxml
<Project Sdk="MSTest.Sdk/4.1.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<UseVSTest>true</UseVSTest>
</PropertyGroup>
</Project>Option B -- Switch CI to : Replace invocations in your CI pipeline with . This works natively with MTP and is the recommended long-term approach for MSTest.Sdk projects.
dotnet testvstest.consoledotnet testIf you need VSTest during a transition period, Option A works without changing CI pipelines.
MSTest.Sdk 默认使用 Microsoft.Testing.Platform (MTP) 模式。在 MTP 模式下,MSTest.Sdk 不会添加对 的引用——仅在 VSTest 模式下才会添加。这不是 v4 特有的变更;MSTest.Sdk v3 也适用该规则。没有 时, 无法发现或运行测试,会静默返回零个测试。这种情况通常出现在迁移过程中,CI 流水线使用 但项目使用默认 MTP 模式的 MSTest.Sdk 时。
Microsoft.NET.Test.SdkMicrosoft.NET.Test.Sdkvstest.consolevstest.console方案A——切换到 VSTest 模式:设置 属性。MSTest.Sdk 会自动添加 :
UseVSTestMicrosoft.NET.Test.Sdkxml
<Project Sdk="MSTest.Sdk/4.1.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<UseVSTest>true</UseVSTest>
</PropertyGroup>
</Project>方案B——将 CI 切换到 :将 CI 流水线中的 调用替换为 。它原生支持 MTP,是 MSTest.Sdk 项目推荐的长期方案。
dotnet testvstest.consoledotnet test如果在过渡期间需要使用 VSTest,方案A无需修改 CI 流水线即可生效。
4.6 Analyzer severity changes
4.6 分析器严重级别变更
Multiple analyzers have been upgraded from Info to Warning by default:
- MSTEST0001, MSTEST0007, MSTEST0017, MSTEST0023, MSTEST0024, MSTEST0025
- MSTEST0030, MSTEST0031, MSTEST0032, MSTEST0035, MSTEST0037, MSTEST0045
Review and fix any new warnings, or suppress them in if intentional.
.editorconfig多个分析器的默认级别已从「信息」提升为「警告」:
- MSTEST0001、MSTEST0007、MSTEST0017、MSTEST0023、MSTEST0024、MSTEST0025
- MSTEST0030、MSTEST0031、MSTEST0032、MSTEST0035、MSTEST0037、MSTEST0045
请检查并修复所有新警告,如果是预期行为可在 中抑制。
.editorconfigStep 5: Verify
步骤5:验证
- Run -- confirm zero errors and review any new warnings
dotnet build - Run -- confirm all tests pass
dotnet test - Compare test results (pass/fail counts) to the pre-migration baseline
- If using Azure DevOps test tracking, be aware that changes may affect history continuity
TestCase.Id - Check that no tests were silently dropped due to stricter discovery
- 执行 ——确认零错误,检查所有新警告
dotnet build - 执行 ——确认所有测试通过
dotnet test - 对比迁移前基线的测试结果(通过/失败数量)
- 如果使用 Azure DevOps 测试跟踪,请注意 变更可能影响历史连续性
TestCase.Id - 检查没有测试因为更严格的发现规则被静默忽略
Validation
验证清单
- All MSTest packages updated to 4.x
- Project builds with zero errors
- All tests pass with
dotnet test - Custom subclasses updated for
TestMethodAttributeand CallerInfoExecuteAsync - replaced with
ExpectedExceptionAttributeAssert.ThrowsExactly - replaced with
Assert.ThrowsException(orAssert.ThrowsExactly)Assert.Throws - enum usages removed
ClassCleanupBehavior - updated to
TestContext.Properties.ContainsContainsKey - All target frameworks are net8.0+, net9.0, net462+, uap10.0.16299, or WinUI
- Behavioral changes reviewed and addressed
- No tests were lost during migration (compare test counts)
- 所有 MSTest 包已更新到 4.x
- 项目构建零错误
- 执行 所有测试通过
dotnet test - 自定义 子类已更新适配
TestMethodAttribute和 CallerInfoExecuteAsync - 已替换为
ExpectedExceptionAttributeAssert.ThrowsExactly - 已替换为
Assert.ThrowsException(或Assert.ThrowsExactly)Assert.Throws - 已移除 枚举的使用
ClassCleanupBehavior - 已更新为
TestContext.Properties.ContainsContainsKey - 所有目标框架为 net8.0+、net9.0、net462+、uap10.0.16299 或 WinUI
- 已审查并处理所有行为变更
- 迁移过程中没有丢失测试(对比测试数量)
Related Skills
相关技能
- -- for modern MSTest v4 assertion APIs and test authoring best practices
writing-mstest-tests - -- for running tests after migration
run-tests
- ——学习现代 MSTest v4 断言 API 和测试编写最佳实践
writing-mstest-tests - ——迁移完成后执行测试
run-tests
Common Pitfalls
常见陷阱
| Pitfall | Solution |
|---|---|
Custom | Change to |
| Use |
| Remove the enum argument; |
| Use |
| Replace with |
| Replace with |
| Use string interpolation: |
| Tests hang that didn't before | AppDomain is disabled by default; on .NET Fx in testhost it is re-enabled automatically |
| Azure DevOps test history breaks | Expected -- |
| Discovery warnings now fail the run | |
| Net6.0/net7.0 targets don't compile | Update to net8.0 -- MSTest v4 supports net8.0, net9.0, net462, uap10.0.16299, modern UWP, and WinUI |
| 陷阱 | 解决方案 |
|---|---|
自定义 | 改为返回 |
| 使用 |
找不到 | 移除枚举参数; |
找不到 | 使用 |
找不到 | 在测试体中替换为 |
找不到 | 替换为 |
带格式化字符串参数的 | 使用字符串插值: |
| 之前正常的测试现在卡住 | AppDomain 默认已禁用;在 .NET Framework 的 testhost 中会自动重新启用 |
| Azure DevOps 测试历史中断 | 预期行为—— |
| 发现警告现在导致运行失败 | |
| Net6.0/net7.0 目标无法编译 | 更新到 net8.0——MSTest v4 支持 net8.0、net9.0、net462、uap10.0.16299、现代 UWP 和 WinUI |
| ", |