Migrate MSTest v1/v2 projects to MSTest v3, and fix v1/v2-to-v3 breaking changes that surface after the packages are already at 3.x.
xUnit -> MSTest Migration
Convert .NET test projects from xUnit.net v2 or v3 to MSTest v4. Use for replacing xunit packages, [Fact]/[Theory], xUnit assertions, fixtures, ITestOutputHelper, traits, skips, and xUnit parallelization with MSTest equivalents while preserving the current VSTest or MTP runner. DO NOT USE FOR: xUnit v2 to v3 upgrades, MSTest version upgrades, migrations from NUnit/TUnit, or runner-only VSTest to MTP migrations.
Workflow
1. Establish the baseline
- In one discovery pass, batch-read the test projects plus
Directory.Build.props,Directory.Packages.props,global.json, and runner configuration, and search the source for the high-risk constructs below. - State the detected source version:
- xunit 2.x and related packages -> xUnit v2 - xunit.v3 or xunit.v3.* -> xUnit v3
- Identify VSTest or MTP from the project and repository configuration. Use
platform-detectiononly when the platform is ambiguous, and preserve the detected platform. - Record the target frameworks and stop if MSTest v4 does not support them.
- If the Fast Path requires a new baseline, run the existing test command once and record discovered, passed, failed, and skipped counts.
- Inventory high-risk constructs before editing:
- IClassFixture, ICollectionFixture, CollectionDefinition, custom FactAttribute/TheoryAttribute/DataAttribute - Assert.Throws, ThrowsAny, IsType, Record.Exception, event assertions - ITestOutputHelper, TestContext.Current, IAsyncLifetime - CollectionBehavior, xunit.runner.json, shared static or external state
2. Replace packages without switching runners
Remove xUnit packages from project files and central package files. This includes xunit*, xunit.v3.*, xunit.runner.visualstudio, YTest.MTP.XUnit2, and xUnit-specific companion packages that are being replaced.
Default to the MSTest v4 metapackage for an incremental conversion:
<!-- Example pin: replace with the exact stable v4 version resolved from the configured package source. -->
<PackageReference Include="MSTest" Version="4.1.0" />
The metapackage includes Microsoft.NET.Test.Sdk, MSTest.TestAdapter, MSTest.TestFramework, and MSTest.Analyzers. When preserving VSTest, prefer retaining a source project's explicit Microsoft.NET.Test.Sdk pin and update it to a version compatible with the chosen MSTest version; this keeps runner/version compatibility reviewable. A repository that intentionally relies on the metapackage's transitive dependency may preserve that convention instead. For the example pin above, MSTest 4.1.0 requires Microsoft.NET.Test.Sdk 18.0.1+; incompatible older pins can cause NU1605.
When preserving MTP, do not carry xUnit's UseMicrosoftTestingPlatformRunner property into the MSTest project. Prefer MSTest.Sdk at the resolved version. If repository conventions require the metapackage route, set <EnableMSTestRunner>true</EnableMSTestRunner> and <OutputType>Exe</OutputType>. Retain TestingPlatformDotnetTestSupport=true only for repositories that continue to invoke MTP applications through VSTest command mode; native .NET 10+ MTP mode does not require it. When preserving VSTest with MSTest.Sdk, set <UseVSTest>true</UseVSTest>.
Do not change TargetFramework. Remove xunit.runner.json only after porting its relevant settings.
3. Perform the mechanical conversion
Apply the common rewrites first:
| xUnit | MSTest | |---|---| | no class attribute | [TestClass] | | [Fact] | [TestMethod] | | [Theory] + [InlineData] | [TestMethod] + [DataRow] | | [MemberData] | [DynamicData] | | [Fact(Skip = "...")] | [TestMethod] + [Ignore("...")] | | [Trait("Category", value)] | [TestCategory(value)] | | [Trait("Owner", value)] | [Owner(value)] | | other [Trait(key, value)] | [TestProperty(key, value)] | | Assert.Equal / NotEqual | Assert.AreEqual / AreNotEqual | | Assert.True / False | Assert.IsTrue / IsFalse | | Assert.Null / NotNull | Assert.IsNull / IsNotNull |
Remove using Xunit; and using Xunit.Abstractions;. Add using Microsoft.VisualStudio.TestTools.UnitTesting; for the metapackage option; MSTest.Sdk supplies it as an implicit global using.
Preserve existing class inheritance. Do not mechanically seal classes.
4. Resolve semantic mappings
Load the mapping cheatsheet for every high-risk construct found in Step 1. These rules are mandatory:
- xUnit
Assert.Throws<T>is exact-type and maps to MSTestAssert.ThrowsExactly<T>. - xUnit
Assert.ThrowsAny<T>permits derived types and maps to MSTestAssert.Throws<T>. - xUnit
Assert.IsType<T>is exact-type and maps toAssert.IsExactInstanceOfType<T>;Assert.IsAssignableFrom<T>maps toAssert.IsInstanceOfType<T>. - xUnit
Assert.Equalon sequences compares elements. UseAssert.AreSequenceEqualon MSTest 4.3+ orCollectionAssert.AreEqualwith materialized lists on earlier v4; never replace sequence equality with reference-basedAssert.AreEqual. [Ignore]and[Timeout]are modifiers; keep[TestMethod]so the test is discovered.[DataRow]values must exactly match parameter types.TestContext.Current.CancellationTokenmaps to an injected MSTestTestContext.CancellationToken; never replace it withCancellationToken.Noneor a newCancellationTokenSource.Owneris a reserved VSTest property. Map[Trait("Owner", value)]to[Owner(value)], not[TestProperty("Owner", value)].- Assertions with no MSTest equivalent (
Assert.Collection,Assert.All,Assert.Equivalent,Record.Exception, event assertions) require an explicit manual rewrite. Never delete an assertion without replacing its verification.
Apply the mechanical and semantic rewrites in one edit pass when the inventory makes the required mappings clear. Do not run an intermediate build by default; use compiler errors from final verification to drive only unresolved conversions.
5. Preserve lifecycle, fixture scope, and parallelization
- Keep constructor setup and
IDisposable/IAsyncDisposablewhen valid. MapIAsyncLifetimeto[TestInitialize]/[TestCleanup]. - Map
IClassFixture<T>to class-scoped initialization and cleanup. - For
ICollectionFixture<T>, preserve both sharing and serialization. Prefer a staticLazy<T>helper used by each member class; add[DoNotParallelize]only when the source collection disabled parallelization. Use assembly initialization only when the fixture is genuinely assembly-wide. - Replace
ITestOutputHelperwith injected or property-based MSTestTestContext.
xUnit runs classes in parallel by default; MSTest runs them serially. Unless the source disabled parallelism, preserve xUnit behavior with:
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
Never use ExecutionScope.MethodLevel to emulate xUnit. Before applying a fixture-scope or parallelization decision, state what the source shared or serialized and how the target preserves it.
6. Verify parity
- Run tests once with the same platform, filter, and configuration used for the baseline.
dotnet testbuilds by default; run a separate build only when needed to isolate a compilation failure. - Compare discovered, passed, failed, and skipped counts.
- Investigate every difference before declaring completion:
- missing cases -> discovery attributes, DynamicData, or DataRow literal types - changed exception behavior -> exact-vs-derived assertion mapping - shared-state failures or large duration changes -> fixture scope and parallelization - silently skipped tests -> missing [TestMethod] or incorrect runtime-skip conversion
- Confirm no xUnit package, namespace, attribute, runner configuration, or fixture interface remains unless explicitly documented for manual follow-up.
- Read back any runner or parallelization configuration you changed and report its file path and effective setting.
Related skills
Fix build errors and breaking changes after upgrading MSTest v3 to v4, or plan a complete v3-to-v4 migration.
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP).