Best for
- 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
dotnet/skills/plugins/dotnet-test-migration/skills/migrate-mstest-v3-to-v4/SKILL.md
Fix build errors and breaking changes after upgrading MSTest v3 to v4, or plan a complete v3-to-v4 migration. Use when user says "upgrade to MSTest v4", "MSTest 4 migration", "MSTest v4 breaking changes", "tests don't compile after upgrading MSTest 3.x to 4.x", or hits CS0507, CS0103, CS1061, CS1615 after updating MSTest packages to 4.x. USE FOR: Execute to ExecuteAsync, CallerInfo ctor on TestMethodAttribute, sealed custom attributes, ClassCleanupBehavior removal, TestContext.Properties Contain
Decision brief
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…
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test-migration/skills/migrate-mstest-v3-to-v4"Inspect the Agent Skill "migrate-mstest-v3-to-v4" from https://github.com/dotnet/skills/blob/1b896e91feb0f613cb54a914f1efd2897810ae02/plugins/dotnet-test-migration/skills/migrate-mstest-v3-to-v4/SKILL.md at commit 1b896e91feb0f613cb54a914f1efd2897810ae02. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
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.
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-mst…
If using the MSTest metapackage:
Work through compilation errors systematically. Use this quick-lookup table to identify all applicable changes, then apply each fix:
These changes won't cause build errors but may affect test runtime behavior.
Permission review
The documentation includes network, browsing, or remote request actions.
await client.GetAsync("https://localhost:0");The documentation includes network, browsing, or remote request actions.
() => client.GetAsync("https://localhost:0"));Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 95/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 5,248 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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.TestFramework, MSTest.TestAdapter, or MSTest metapackage from 3.x to 4.xMSTest.Sdk from 3.x to 4.xTestMethodAttribute or ConditionBaseAttribute implementations for v4migrate-mstest-v1v2-to-v3 first, then return here| Input | Required | Description |
|---|---|---|
| Project or solution path | No | The .csproj, .sln, or .slnx entry point containing MSTest test projects. Discover it yourself by globbing the working directory; ask only when nothing is found or the choice is genuinely ambiguous |
| Build command | No | How to build (e.g., dotnet build, a repo build script). Auto-detect if not provided |
| Test command | No | How to run tests (e.g., dotnet test). Auto-detect if not provided |
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.dotnet test). Do not walk through the full migration 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.
MSTest, MSTest.TestFramework, MSTest.TestAdapter, or MSTest.Sdk in .csproj, Directory.Build.props, or Directory.Packages.props.migrate-mstest-v1v2-to-v3 first.TestMethodAttribute subclasses -- these require changes in v4.ExpectedExceptionAttribute -- removed in v4 (deprecated since v3 with analyzer MSTEST0006).Assert.ThrowsException (deprecated) -- removed in v4.If using the MSTest metapackage:
<PackageReference Include="MSTest" Version="4.1.0" />
If using individual packages:
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
If using MSTest.Sdk:
<Project Sdk="MSTest.Sdk/4.1.0">
Run dotnet restore, then dotnet build. Collect all errors for Step 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 TestMethodAttribute overrides Execute | Execute removed | Change to ExecuteAsync returning Task<TestResult[]> (3.1) |
[TestMethod("name")] or custom attribute constructor | CallerInfo params added | Use DisplayName = "name" named param; propagate CallerInfo in subclasses (3.2) |
ClassCleanupBehavior.EndOfClass | Enum removed | Remove 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 removed | Use FullyQualifiedTestClassName (3.6) |
Assert.AreEqual(a, b, "msg {0}", arg) | Message+params overloads removed | Use string interpolation: $"msg {arg}" (3.7) |
Assert.ThrowsException<T>(...) | Renamed | Replace with Assert.ThrowsExactly<T>(...) or Assert.Throws<T>(...) (3.7) |
Assert.IsInstanceOfType<T>(obj, out var t) | Out parameter removed | Use var t = Assert.IsInstanceOfType<T>(obj) (3.7) |
[ExpectedException(typeof(T))] | Attribute removed | Move assertion into test body: Assert.ThrowsExactly<T>(() => ...) (3.8) |
| 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.
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.
// 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 now uses [CallerFilePath] and [CallerLineNumber] parameters in its constructor.
If you inherit from TestMethodAttribute, propagate caller info to the base class:
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:
// Before (v3)
[TestMethodAttribute("Custom display name")]
// After (v4)
[TestMethodAttribute(DisplayName = "Custom display name")]
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:
// 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.
TestContext.Properties changed from IDictionary to IDictionary<string, object>. Update any Contains calls to ContainsKey:
// Before (v3)
testContext.Properties.Contains("key");
// After (v4)
testContext.Properties.ContainsKey("key");
The TestTimeout enum (with only TestTimeout.Infinite) is removed. Replace with int.MaxValue:
// Before (v3)
[Timeout(TestTimeout.Infinite)]
// After (v4)
[Timeout(int.MaxValue)]
The TestContext.ManagedType property is removed. Use TestContext.FullyQualifiedTestClassName instead.
message and object[] parameters now accept only message. Use string interpolation instead of format strings:// 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 APIs are renamed. Use Assert.ThrowsExactly (strict type match) or Assert.Throws (accepts derived exception types):// 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<T>(x, out var t) changes to var t = Assert.IsInstanceOfType<T>(x):// Before (v3)
Assert.IsInstanceOfType<MyType>(obj, out var typed);
// After (v4)
var typed = Assert.IsInstanceOfType<MyType>(obj);
object.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:
// 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:
// 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:
// 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).
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:
<!-- Before -->
<TargetFramework>net6.0</TargetFramework>
<!-- After -->
<TargetFramework>net8.0</TargetFramework>
The UnfoldingStrategy property (introduced in MSTest 3.7) has moved from individual data source attributes (DataRowAttribute, DynamicDataAttribute) to TestMethodAttribute.
The ConditionBaseAttribute.ShouldRun property is renamed to IsConditionMet.
Several types previously public are now internal or removed:
MSTestDiscoverer, MSTestExecutor, AssemblyResolver, LogMessageListenerTestExecutionManager, TestMethodInfo, TestResultExtensionsUnitTestOutcomeExtensions, GenericParameterHelperITestMethod in PlatformServices assembly (the one in TestFramework is unchanged)If your code references any of these, find alternative approaches or remove the dependency.
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 | 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 before | AppDomain disabled by default (4.1) | Set DisableAppDomain to false in .runsettings RunConfiguration |
| vstest.console can't find tests with MSTest.Sdk | MSTest.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 analyzers | Analyzer severities upgraded (4.6) | Fix warnings or suppress in .editorconfig |
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:
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>
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.
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.
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:
<RunSettings>
<MSTest>
<TreatDiscoveryWarningsAsErrors>false</TreatDiscoveryWarningsAsErrors>
</MSTest>
</RunSettings>
Recommended: Fix the underlying discovery warnings rather than suppressing this setting.
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:
<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.
Multiple analyzers have been upgraded from Info to Warning by default:
Review and fix any new warnings, or suppress them in .editorconfig if intentional.
dotnet build -- confirm zero errors and review any new warningsdotnet test -- confirm all tests passTestCase.Id changes may affect history continuitydotnet testTestMethodAttribute subclasses updated for ExecuteAsync and CallerInfoExpectedExceptionAttribute replaced with Assert.ThrowsExactlyAssert.ThrowsException replaced with Assert.ThrowsExactly (or Assert.Throws)ClassCleanupBehavior enum usages removedTestContext.Properties.Contains updated to ContainsKeywriting-mstest-tests -- for modern MSTest v4 assertion APIs and test authoring best practicesrun-tests -- for running tests after migration| Pitfall | Solution |
|---|---|
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 before | AppDomain is disabled by default; on .NET Fx in testhost it is re-enabled automatically |
| Azure DevOps test history breaks | Expected -- 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 compile | Update to net8.0 -- MSTest v4 supports net8.0, net9.0, net462, uap10.0.16299, modern UWP, and WinUI |
Frequently asked questions
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…
The source record exposes this install command: npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test-migration/skills/migrate-mstest-v3-to-v4". Inspect the command and pinned source before running it.
Static rules flagged network in the source; the page lists the matching lines and excerpts.
Alternatives
garrytan/gbrain
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing
vipshop/cache-dit
High-level guide for integrating a new DiT model into cache-dit: Cache (BlockAdapter/ForwardPattern), Context Parallelism, Tensor Parallelism, Text Encoder Parallelism (TE-P), VAE Parallelism (VAE-P), generate CLI, installation, testing workflow, and detailed references. Use when adding support for a new diffusion transformer model in cache-dit.