migrate-mstest-v3-to-v4

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MSTest 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
    ,
    MSTest.TestAdapter
    , or
    MSTest
    metapackage from 3.x to 4.x
  • Upgrading
    MSTest.Sdk
    from 3.x to 4.x
  • Fixing build errors after updating to MSTest v4 packages
  • Resolving behavioral changes in test execution after upgrading to MSTest v4
  • Updating custom
    TestMethodAttribute
    or
    ConditionBaseAttribute
    implementations for v4
  • MSTest.TestFramework
    MSTest.TestAdapter
    或 MSTest 元包从 3.x 升级到 4.x
  • MSTest.Sdk
    从 3.x 升级到 4.x
  • 修复升级到 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
    migrate-mstest-v1v2-to-v3
    first, then return here
  • 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

输入参数

InputRequiredDescription
Project or solution pathYesThe
.csproj
,
.sln
, or
.slnx
entry point containing MSTest test projects
Build commandNoHow to build (e.g.,
dotnet build
, a repo build script). Auto-detect if not provided
Test commandNoHow to run tests (e.g.,
dotnet test
). Auto-detect if not provided
输入必填描述
项目或解决方案路径包含 MSTest 测试项目的
.csproj
.sln
.slnx
入口文件
构建命令构建方式(例如
dotnet build
、仓库自定义构建脚本),未提供则自动检测
测试命令测试执行方式(例如
dotnet test
),未提供则自动检测

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
    TestMethodAttribute
    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.
  • "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
    dotnet test
    ). Do not walk through the full migration workflow.
  • 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中相关的破坏性变更。始终基于用户实际的类型和方法名提供具体的修复代码——展示完整可直接复制粘贴的代码片段,而不仅仅是修改说明。对于自定义
    TestMethodAttribute
    子类,展示完整的修复后类,包括向基构造函数传递 CallerInfo 的逻辑。提及可以提前发现此类问题的相关分析器(例如对应 ExpectedException 的 MSTEST0006)。无需遍历完整迁移流程。
  • 「预期影响」类问题(用户在升级前询问破坏性变更):展示步骤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:评估项目

  1. Identify the current MSTest version by checking package references for
    MSTest
    ,
    MSTest.TestFramework
    ,
    MSTest.TestAdapter
    , or
    MSTest.Sdk
    in
    .csproj
    ,
    Directory.Build.props
    , or
    Directory.Packages.props
    .
  2. Confirm the project is on MSTest v3 (3.x). If on v1 or v2, use
    migrate-mstest-v1v2-to-v3
    first.
  3. 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).
  4. Check for custom
    TestMethodAttribute
    subclasses -- these require changes in v4.
  5. Check for usages of
    ExpectedExceptionAttribute
    -- removed in v4 (deprecated since v3 with analyzer MSTEST0006).
  6. Check for usages of
    Assert.ThrowsException
    (deprecated) -- removed in v4.
  7. Run a clean build to establish a baseline of existing errors/warnings.
  1. 检查
    .csproj
    Directory.Build.props
    Directory.Packages.props
    MSTest
    MSTest.TestFramework
    MSTest.TestAdapter
    MSTest.Sdk
    的包引用,确认当前 MSTest 版本。
  2. 确认项目使用 MSTest v3 (3.x)。如果是 v1 或 v2,请先使用
    migrate-mstest-v1v2-to-v3
  3. 检查目标框架——MSTest v4 不再支持 .NET Core 3.1 到 .NET 7 版本。支持的目标框架包括:net8.0net9.0net462(.NET Framework 4.6.2+)、uap10.0.16299(UWP)、net9.0-windows10.0.17763.0(现代UWP)、net8.0-windows10.0.18362.0(WinUI)。
  4. 检查是否有自定义
    TestMethodAttribute
    子类——v4 中这类代码需要修改。
  5. 检查是否使用了
    ExpectedExceptionAttribute
    ——v4 中已移除(从 v3 开始已废弃,对应分析器 MSTEST0006)。
  6. 检查是否使用了
    Assert.ThrowsException
    (已废弃)——v4 中已移除。
  7. 执行清理构建,确认现有错误/警告基线。

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
dotnet restore
, then
dotnet build
. Collect all errors for Step 3.
如果使用 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">
执行
dotnet restore
,然后执行
dotnet build
,收集所有错误用于步骤3处理。

Step 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 codeBreaking changeFix
Custom
TestMethodAttribute
overrides
Execute
Execute removedChange to
ExecuteAsync
returning
Task<TestResult[]>
(3.1)
[TestMethod("name")]
or custom attribute constructor
CallerInfo params addedUse
DisplayName = "name"
named param; propagate CallerInfo in subclasses (3.2)
ClassCleanupBehavior.EndOfClass
Enum removedRemove argument: just
[ClassCleanup]
(3.3)
TestContext.Properties.Contains("key")
Properties
is
IDictionary<string, object>
Change to
ContainsKey("key")
(3.4)
[Timeout(TestTimeout.Infinite)]
TestTimeout
enum removed
Replace with
[Timeout(int.MaxValue)]
(3.5)
TestContext.ManagedType
Property removedUse
FullyQualifiedTestClassName
(3.6)
Assert.AreEqual(a, b, "msg {0}", arg)
Message+params overloads removedUse string interpolation:
$"msg {arg}"
(3.7)
Assert.ThrowsException<T>(...)
RenamedReplace with
Assert.ThrowsExactly<T>(...)
or
Assert.Throws<T>(...)
(3.7)
Assert.IsInstanceOfType<T>(obj, out var t)
Out parameter removedUse
var t = Assert.IsInstanceOfType<T>(obj)
(3.7)
[ExpectedException(typeof(T))]
Attribute removedMove assertion into test body:
Assert.ThrowsExactly<T>(() => ...)
(3.8)
Project targets net5.0, net6.0, or net7.0TFM droppedChange 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.
系统性处理编译错误。使用下方快速查询表识别所有适用变更,然后逐一应用修复:
代码中的错误/模式破坏性变更修复方案
自定义
TestMethodAttribute
重写了
Execute
Execute 已移除改为返回
Task<TestResult[]>
ExecuteAsync
(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)
移除了 out 参数改为
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

3.1 TestMethodAttribute.Execute -> ExecuteAsync

If you have custom
TestMethodAttribute
subclasses that override
Execute
, change to
ExecuteAsync
. This change was made because the v3 synchronous
Execute
API caused deadlocks when test code used
async
/
await
internally -- the synchronous wrapper would block the thread while the async operation needed that same thread to complete.
csharp
// 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
Task.FromResult
when your override logic is purely synchronous. Use
async
/
await
when you call
base.ExecuteAsync
or other async methods.
如果你的自定义
TestMethodAttribute
子类重写了
Execute
,请改为
ExecuteAsync
。该变更的原因是 v3 的同步
Execute
API 会在测试代码内部使用
async
/
await
时导致死锁——同步包装器会阻塞线程,而异步操作需要同一个线程才能完成。
csharp
// 改造前 (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.FromResult
。如果调用了
base.ExecuteAsync
或其他异步方法,使用
async
/
await

3.2 TestMethodAttribute CallerInfo constructor

3.2 TestMethodAttribute CallerInfo 构造函数

TestMethodAttribute
now uses
[CallerFilePath]
and
[CallerLineNumber]
parameters in its constructor.
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
[TestMethodAttribute("Custom display name")]
, switch to the named parameter syntax:
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
ClassCleanupBehavior
enum is removed. In v3, this enum controlled whether class cleanup ran at end of class (
EndOfClass
) or end of assembly (
EndOfAssembly
). In v4, class cleanup always runs at end of class. Remove the enum argument:
csharp
// Before (v3)
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void ClassCleanup(TestContext testContext) { }

// After (v4)
[ClassCleanup]
public static void ClassCleanup(TestContext testContext) { }
If you previously used
ClassCleanupBehavior.EndOfAssembly
, move that cleanup logic to an
[AssemblyCleanup]
method instead.
ClassCleanupBehavior
枚举已被移除。在 v3 中,该枚举用于控制类清理是在类结束时执行(
EndOfClass
)还是程序集结束时执行(
EndOfAssembly
)。在 v4 中,类清理始终在类结束时执行。移除枚举参数即可:
csharp
// 改造前 (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.Properties
changed from
IDictionary
to
IDictionary<string, object>
. Update any
Contains
calls to
ContainsKey
:
csharp
// Before (v3)
testContext.Properties.Contains("key");

// After (v4)
testContext.Properties.ContainsKey("key");
TestContext.Properties
IDictionary
变为
IDictionary<string, object>
。将所有
Contains
调用改为
ContainsKey
csharp
// 改造前 (v3)
testContext.Properties.Contains("key");

// 改造后 (v4)
testContext.Properties.ContainsKey("key");

3.5 TestTimeout enum removed

3.5 TestTimeout 枚举已移除

The
TestTimeout
enum (with only
TestTimeout.Infinite
) is removed. Replace with
int.MaxValue
:
csharp
// Before (v3)
[Timeout(TestTimeout.Infinite)]

// After (v4)
[Timeout(int.MaxValue)]
仅包含
TestTimeout.Infinite
TestTimeout
枚举已被移除。替换为
int.MaxValue
即可:
csharp
// 改造前 (v3)
[Timeout(TestTimeout.Infinite)]

// 改造后 (v4)
[Timeout(int.MaxValue)]

3.6 TestContext.ManagedType removed

3.6 TestContext.ManagedType 已移除

The
TestContext.ManagedType
property is removed. Use
TestContext.FullyQualifiedTestClassName
instead.
TestContext.ManagedType
属性已被移除,请使用
TestContext.FullyQualifiedTestClassName
替代。

3.7 Assert API signature changes

3.7 Assert API 签名变更

  • Message + params removed: Assert methods that accepted both
    message
    and
    object[]
    parameters now accept only
    message
    . Use string interpolation instead of format strings:
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
    Assert.ThrowsException
    APIs are renamed. Use
    Assert.ThrowsExactly
    (strict type match) or
    Assert.Throws
    (accepts derived exception types):
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:
    Assert.IsInstanceOfType<T>(x, out var t)
    changes to
    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
    object[]
    参数的 Assert 方法现在仅接受
    message
    。使用字符串插值替代格式化字符串:
csharp
// 改造前 (v3)
Assert.AreEqual(expected, actual, "预期值为 {0},实际值为 {1}", expected, actual);

// 改造后 (v4)
Assert.AreEqual(expected, actual, $"预期值为 {expected},实际值为 {actual}");
  • Assert.ThrowsException 已重命名
    Assert.ThrowsException
    API 已重命名。使用
    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
[ExpectedException]
attribute is removed in v4. In MSTest 3.2, the
MSTEST0006
analyzer was introduced to flag
[ExpectedException]
usage and suggest migrating to
Assert.ThrowsExactly
while still on v3 (a non-breaking change). In v4, the attribute is gone entirely. Migrate to
Assert.ThrowsExactly
:
csharp
// 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.ThrowsExactlyAsync
:
csharp
// 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
[ExpectedException]
used the
AllowDerivedTypes
property
, use
Assert.ThrowsAsync<T>
(base type matching) instead of
Assert.ThrowsExactlyAsync<T>
(exact type matching).
[ExpectedException]
特性在 v4 中已被移除。在 MSTest 3.2 中引入了
MSTEST0006
分析器,用于标记
[ExpectedException]
的使用,并建议在 v3 阶段就迁移到
Assert.ThrowsExactly
(非破坏性变更)。在 v4 中该特性已完全移除,请迁移到
Assert.ThrowsExactly
csharp
// 改造前 (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.ThrowsExactlyAsync
csharp
// 改造前 (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]
使用了
AllowDerivedTypes
属性
,请使用
Assert.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
TargetFramework
:
xml
<!-- Before -->
<TargetFramework>net6.0</TargetFramework>

<!-- After -->
<TargetFramework>net8.0</TargetFramework>
MSTest v4 支持的框架包括:net8.0net9.0net462(.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。
如果测试项目的目标框架不受支持,请更新
TargetFramework
xml
<!-- 改造前 -->
<TargetFramework>net6.0</TargetFramework>

<!-- 改造后 -->
<TargetFramework>net8.0</TargetFramework>

3.10 Unfolding strategy moved to TestMethodAttribute

3.10 展开策略移到 TestMethodAttribute

The
UnfoldingStrategy
property (introduced in MSTest 3.7) has moved from individual data source attributes (
DataRowAttribute
,
DynamicDataAttribute
) to
TestMethodAttribute
.
MSTest 3.7 引入的
UnfoldingStrategy
属性已从单个数据源特性(
DataRowAttribute
DynamicDataAttribute
)移到
TestMethodAttribute

3.11 ConditionBaseAttribute.ShouldRun renamed

3.11 ConditionBaseAttribute.ShouldRun 已重命名

The
ConditionBaseAttribute.ShouldRun
property is renamed to
IsConditionMet
.
ConditionBaseAttribute.ShouldRun
属性已重命名为
IsConditionMet

3.12 Internal/removed types

3.12 内部/已移除类型

Several types previously public are now internal or removed:
  • MSTestDiscoverer
    ,
    MSTestExecutor
    ,
    AssemblyResolver
    ,
    LogMessageListener
  • TestExecutionManager
    ,
    TestMethodInfo
    ,
    TestResultExtensions
  • UnitTestOutcomeExtensions
    ,
    GenericParameterHelper
  • ITestMethod
    in PlatformServices assembly (the one in TestFramework is unchanged)
If your code references any of these, find alternative approaches or remove the dependency.
多个之前公开的类型现在已改为内部类型或被移除:
  • MSTestDiscoverer
    MSTestExecutor
    AssemblyResolver
    LogMessageListener
  • TestExecutionManager
    TestMethodInfo
    TestResultExtensions
  • UnitTestOutcomeExtensions
    GenericParameterHelper
  • PlatformServices 程序集中的
    ITestMethod
    (TestFramework 中的对应类型无变化)
如果你的代码引用了上述任何类型,请寻找替代方案或移除依赖。

Step 4: Address behavioral changes

步骤4:处理行为变更

These changes won't cause build errors but may affect test runtime behavior.
SymptomCauseFix
Tests show as new in Azure DevOps / test history lost
TestCase.Id
generation changed (4.3)
No code fix; history will re-baseline
TestContext.TestName
throws in
[ClassInitialize]
v4 enforces lifecycle scope (4.2)Move access to
[TestInitialize]
or test methods
Tests not discovered / discovery failures
TreatDiscoveryWarningsAsErrors
now true (4.4)
Fix warnings, or set to false in .runsettings
Tests hang that didn't beforeAppDomain disabled by default (4.1)Set
DisableAppDomain
to false in .runsettings
RunConfiguration
vstest.console can't find tests with MSTest.SdkMSTest.Sdk defaults to MTP;
Microsoft.NET.Test.Sdk
only added in VSTest mode (4.5)
Add explicit package reference or switch to
dotnet test
New warnings from analyzersAnalyzer severities upgraded (4.6)Fix warnings or suppress in .editorconfig
这些变更不会导致构建错误,但可能影响测试运行时行为。
症状原因修复方案
Azure DevOps 中测试显示为新用例 / 测试历史丢失
TestCase.Id
生成规则变更 (4.3)
无需代码修复;历史记录会重新建立基线
TestContext.TestName
[ClassInitialize]
中抛出异常
v4 强制生命周期范围限制 (4.2)将访问逻辑移到
[TestInitialize]
或测试方法中
测试未被发现 / 发现失败
TreatDiscoveryWarningsAsErrors
现在默认值为 true (4.4)
修复警告,或在 .runsettings 中设为 false
之前正常的测试现在卡住AppDomain 默认禁用 (4.1)在 .runsettings 的
RunConfiguration
中将
DisableAppDomain
设为 false
使用 MSTest.Sdk 时 vstest.console 找不到测试MSTest.Sdk 默认使用 MTP;仅在 VSTest 模式下才会添加
Microsoft.NET.Test.Sdk
(4.5)
显式添加包引用,或切换到
dotnet test
分析器抛出新警告分析器严重级别提升 (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
dotnet test
and VS), MSTest re-enables AppDomains automatically. If you need to explicitly control AppDomain isolation, set it via
.runsettings
:
xml
<RunSettings>
  <RunConfiguration>
    <DisableAppDomain>false</DisableAppDomain>
  </RunConfiguration>
</RunSettings>
AppDomain 默认已禁用。在 .NET Framework 中,当运行在 testhost 中时(
dotnet test
和 VS 的默认模式),MSTest 会自动重新启用 AppDomain。如果你需要显式控制 AppDomain 隔离,可以通过
.runsettings
设置:
xml
<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:
  • TestContext.FullyQualifiedTestClassName
    -- cannot be accessed in
    [AssemblyInitialize]
  • TestContext.TestName
    -- cannot be accessed in
    [AssemblyInitialize]
    or
    [ClassInitialize]
Fix: Move any code that accesses
TestContext.TestName
from
[ClassInitialize]
to
[TestInitialize]
or individual test methods, where per-test context is available. Do not replace
TestName
with
FullyQualifiedTestClassName
as a workaround -- they have different semantics.
MSTest v4 现在会在错误的生命周期阶段访问测试专属属性时抛出异常:
  • TestContext.FullyQualifiedTestClassName
    ——不能在
    [AssemblyInitialize]
    中访问
  • TestContext.TestName
    ——不能在
    [AssemblyInitialize]
    [ClassInitialize]
    中访问
修复方案:将所有访问
TestContext.TestName
的代码从
[ClassInitialize]
移到
[TestInitialize]
或单个测试方法中,这些阶段可以获取到每个测试的上下文。不要用
FullyQualifiedTestClassName
替代
TestName
作为临时方案——二者语义不同。

4.3 TestCase.Id generation changed

4.3 TestCase.Id 生成规则变更

The generation algorithm for
TestCase.Id
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.Id
的生成算法已修改,用于修复长期存在的 bug。这可能会影响 Azure DevOps 测试结果跟踪(例如长期的测试失败跟踪)。无需代码修复,但请注意测试结果历史会出现断层。

4.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
Microsoft.NET.Test.Sdk
-- it only adds it in VSTest mode. This is not a v4-specific change; it applies to MSTest.Sdk v3 as well. Without
Microsoft.NET.Test.Sdk
,
vstest.console
cannot discover or run tests and will silently find zero tests. This commonly surfaces during migration when a CI pipeline uses
vstest.console
but the project uses MSTest.Sdk in its default MTP mode.
Option A -- Switch to VSTest mode: Set the
UseVSTest
property. MSTest.Sdk will then automatically add
Microsoft.NET.Test.Sdk
:
xml
<Project Sdk="MSTest.Sdk/4.1.0">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <UseVSTest>true</UseVSTest>
  </PropertyGroup>
</Project>
Option B -- Switch CI to
dotnet test
: Replace
vstest.console
invocations in your CI pipeline with
dotnet test
. This works natively with MTP and is the recommended long-term approach for MSTest.Sdk projects.
If you need VSTest during a transition period, Option A works without changing CI pipelines.
MSTest.Sdk 默认使用 Microsoft.Testing.Platform (MTP) 模式。在 MTP 模式下,MSTest.Sdk 不会添加对
Microsoft.NET.Test.Sdk
的引用——仅在 VSTest 模式下才会添加。这不是 v4 特有的变更;MSTest.Sdk v3 也适用该规则。没有
Microsoft.NET.Test.Sdk
时,
vstest.console
无法发现或运行测试,会静默返回零个测试。这种情况通常出现在迁移过程中,CI 流水线使用
vstest.console
但项目使用默认 MTP 模式的 MSTest.Sdk 时。
方案A——切换到 VSTest 模式:设置
UseVSTest
属性。MSTest.Sdk 会自动添加
Microsoft.NET.Test.Sdk
xml
<Project Sdk="MSTest.Sdk/4.1.0">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <UseVSTest>true</UseVSTest>
  </PropertyGroup>
</Project>
方案B——将 CI 切换到
dotnet test
:将 CI 流水线中的
vstest.console
调用替换为
dotnet test
。它原生支持 MTP,是 MSTest.Sdk 项目推荐的长期方案。
如果在过渡期间需要使用 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
.editorconfig
if intentional.
多个分析器的默认级别已从「信息」提升为「警告」:
  • MSTEST0001、MSTEST0007、MSTEST0017、MSTEST0023、MSTEST0024、MSTEST0025
  • MSTEST0030、MSTEST0031、MSTEST0032、MSTEST0035、MSTEST0037、MSTEST0045
请检查并修复所有新警告,如果是预期行为可在
.editorconfig
中抑制。

Step 5: Verify

步骤5:验证

  1. Run
    dotnet build
    -- confirm zero errors and review any new warnings
  2. Run
    dotnet test
    -- confirm all tests pass
  3. Compare test results (pass/fail counts) to the pre-migration baseline
  4. If using Azure DevOps test tracking, be aware that
    TestCase.Id
    changes may affect history continuity
  5. Check that no tests were silently dropped due to stricter discovery
  1. 执行
    dotnet build
    ——确认零错误,检查所有新警告
  2. 执行
    dotnet test
    ——确认所有测试通过
  3. 对比迁移前基线的测试结果(通过/失败数量)
  4. 如果使用 Azure DevOps 测试跟踪,请注意
    TestCase.Id
    变更可能影响历史连续性
  5. 检查没有测试因为更严格的发现规则被静默忽略

Validation

验证清单

  • All MSTest packages updated to 4.x
  • Project builds with zero errors
  • All tests pass with
    dotnet test
  • Custom
    TestMethodAttribute
    subclasses updated for
    ExecuteAsync
    and CallerInfo
  • ExpectedExceptionAttribute
    replaced with
    Assert.ThrowsExactly
  • Assert.ThrowsException
    replaced with
    Assert.ThrowsExactly
    (or
    Assert.Throws
    )
  • ClassCleanupBehavior
    enum usages removed
  • TestContext.Properties.Contains
    updated to
    ContainsKey
  • 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
    子类已更新适配
    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
  • 已审查并处理所有行为变更
  • 迁移过程中没有丢失测试(对比测试数量)

Related Skills

相关技能

  • writing-mstest-tests
    -- for modern MSTest v4 assertion APIs and test authoring best practices
  • run-tests
    -- for running tests after migration
  • writing-mstest-tests
    ——学习现代 MSTest v4 断言 API 和测试编写最佳实践
  • run-tests
    ——迁移完成后执行测试

Common Pitfalls

常见陷阱

PitfallSolution
Custom
TestMethodAttribute
still overrides
Execute
Change to
ExecuteAsync
returning
Task<TestResult[]>
TestMethodAttribute("display name")
no longer compiles
Use
TestMethodAttribute(DisplayName = "display name")
ClassCleanupBehavior
enum not found
Remove the enum argument;
[ClassCleanup]
now always runs at end of class. For end-of-assembly cleanup, use
[AssemblyCleanup]
TestContext.Properties.Contains
missing
Use
ContainsKey
--
Properties
is now
IDictionary<string, object>
ExpectedException
attribute not found
Replace with
Assert.ThrowsExactly<T>(() => ...)
inside the test body
Assert.ThrowsException
not found
Replace with
Assert.ThrowsExactly
(or
Assert.Throws
for derived types)
Assert.AreEqual
with format string args fails
Use string interpolation:
$"message {value}"
Tests hang that didn't beforeAppDomain is disabled by default; on .NET Fx in testhost it is re-enabled automatically
Azure DevOps test history breaksExpected --
TestCase.Id
generation changed; no code fix, results will re-baseline
Discovery warnings now fail the run
TreatDiscoveryWarningsAsErrors
is true by default; fix the discovery warnings
Net6.0/net7.0 targets don't compileUpdate to net8.0 -- MSTest v4 supports net8.0, net9.0, net462, uap10.0.16299, modern UWP, and WinUI
陷阱解决方案
自定义
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 默认已禁用;在 .NET Framework 的 testhost 中会自动重新启用
Azure DevOps 测试历史中断预期行为——
TestCase.Id
生成规则变更;无需代码修复,结果会重新建立基线
发现警告现在导致运行失败
TreatDiscoveryWarningsAsErrors
默认值为 true;修复发现警告即可
Net6.0/net7.0 目标无法编译更新到 net8.0——MSTest v4 支持 net8.0、net9.0、net462、uap10.0.16299、现代 UWP 和 WinUI
",