microsoft/migrate-xunit-to-mstest
> Migrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4. port [Fact]/[Theory]/[InlineData]/[MemberData]/[ClassData] to [TestMethod]/[DataRow]/[DynamicData], port Assert.Equal/True/Throws/ThrowsAsync to Assert.AreEqual/IsTrue/ThrowsExactly/ThrowsExactlyAsync, port IClassFixture/ ICollectionFixture/IDisposable/IAsyncLifetime/ITestOutputHelper/[Trait]/[Fact(Skip)] to MSTest equivalents, preserve xUnit parallel-class default via xUnit, NUnit/TUnit -> MSTest (no skills exist); MSTest version upgrades (use migrate-mstest-v1v2-to-v3 or migrate-mstest-v3-to-v4); VSTest <-> MTP only (use migrate-vstest-to-mtp); general .NET upgrades.
npx skills add https://github.com/microsoft/testfx --skill migrate-xunit-to-mstest
Migrate a .NET test project from xUnit.net (v2 or v3) to MSTest v4. The outcome is a project that:
MSTest.Sdk 4.x) instead of xunit* / xunit.v3.*[Fact]/[Theory] rewritten as [TestMethod] and every assertion mapped to the MSTest equivalentThis is a cross-framework migration. Do not bundle it with a version upgrade or a platform switch in the same pass -- if both are needed, do this skill first, commit, then run migrate-mstest-v3-to-v4 (if you stopped on v3) or migrate-vstest-to-mtp.
xunit, xunit.assert, xunit.core, xunit.extensibility.core/execution, xunit.abstractions, or any xunit.v3.* package, and you want to switch to MSTest| Input | Required | Description |
|-------|----------|-------------|
| Project or solution path | Yes | The .csproj, .sln, or .slnx containing xUnit test projects |
| Build command | No | How to build (e.g., dotnet build). Auto-detect if not provided |
| Test command | No | How to run tests (e.g., dotnet test). Auto-detect if not provided |
xunit 2.x) or xUnit v3 (xunit.v3 / xunit.v3.*) before recommending changes. This grounds the migration advice -- some breaking-change steps only apply to one version.<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>), keep MTP. Recommend migrate-vstest-to-mtp as a separate follow-up only if the user asks for it.[assembly: Parallelize(...)] is required to match xUnit's behaviour -- omitting it silently halves CI throughput.Assert.Throws<T> -> Assert.ThrowsExactly<T> (Step 6): mention the exact-type-vs-any-derived semantic flip so reviewers know the assertion was deliberately renamed, not just translated.The conversion is mechanical for ~80% of code (attributes and simple assertions) and judgement-based for ~20% (collection fixtures, custom data attributes, exact-type-vs-derived exception assertions, parallelization semantics). Always do the mechanical pass first so build errors point you at the judgement areas.
For the full attribute/assertion/fixture/lifecycle mapping tables -- including semantic traps (Assert.Throws<T> vs Assert.ThrowsAny<T>, IClassFixture vs ICollectionFixture scope), edge cases (TheoryData<T...>, MemberType=, custom DataAttribute, custom FactAttribute, Record.Exception), and copy-pasteable before/after snippets -- see references/mapping-cheatsheet.md. Load it whenever you need a specific xUnit -> MSTest equivalent.
For writing idiomatic MSTest code (modern assertion APIs, lifecycle patterns, data-driven conventions, Assert.HasCount/IsEmpty/StartsWith, etc.), see the writing-mstest-tests skill. Do not re-derive idiomatic MSTest patterns here. Apply this skill to *convert*; apply writing-mstest-tests to *polish*.
> Commit strategy: Commit after Step 2 (packages updated, builds broken), after Step 6 (attributes converted, asserts fixed), and after Step 8 (fixtures/lifecycle rewritten, tests pass). Commit before fixing follow-up cleanup so reviewers can bisect.
.csproj, Directory.Build.props, Directory.Packages.props, and global.json.xunit 2.x (+ xunit.assert / xunit.core / xunit.abstractions) -> xUnit v2xunit.v3 / xunit.v3.* -> xUnit v3platform-detection skill. The xUnit/MTP matrix is nuanced -- xunit.v3 inside Test Explorer is MTP by default unless opted out, while xunit.v3 inside dotnet test depends on the xunit.v3.mtp-v* packages -- so do not try to inline a shortcut here. Quick signals to feed into that skill: xunit.runner.visualstudio (v2) usually means VSTest; xunit.v3.mtp-v* / xunit.v3.core.mtp-v* packages or YTest.MTP.XUnit2 (v2 MTP shim) usually mean MTP. <UseMicrosoftTestingPlatformRunner> only affects dotnet run and is not a reliable VSTest-vs-MTP signal on its own.TargetFramework is supported by MSTest v4:net8.0, net9.0, net462+, netstandard2.0 (test library only), uap10.0.16299, net8.0-windows10.0.18362.0 (WinUI), net9.0-windows10.0.17763.0 (modern UWP).net5.0-net7.0. STOP and ask the user to upgrade the TFM first, or migrate to MSTest v3 (then use migrate-mstest-v3-to-v4 after a TFM bump).ICollectionFixture<T> / [CollectionDefinition] (scope concern -- see Step 8)DataAttribute / custom FactAttribute / custom TheoryAttribute subclasses (manual conversion to ITestDataSource / TestMethodAttribute -- see Step 5)Assert.Throws<T> (xUnit semantics = exact type; maps to Assert.ThrowsExactly<T>, not Assert.Throws<T>)Record.Exception / Record.ExceptionAsync (manual conversion)Assert.Raises* / event assertions (no MSTest equivalent -- manual)[assembly: CaptureConsole] and other v3-only assembly attributes[DoNotParallelize], or refactor the shared state.> Choose the package option that matches what the project uses today. When the user says "preserve VSTest" -- or the existing project uses explicit PackageReferences -- default to Option A (MSTest metapackage). Reach for Option B (MSTest.Sdk) only when the user explicitly asks to modernize the SDK or already uses MSTest.Sdk elsewhere in the solution; if you adopt it, you must preserve the platform from Step 1.
Remove every xUnit package reference (from .csproj, Directory.Build.props, Directory.Packages.props):
xunit, xunit.abstractions, xunit.assert, xunit.corexunit.extensibility.core, xunit.extensibility.executionxunit.runner.visualstudioxunit.v3, xunit.v3.assert, xunit.v3.core, xunit.v3.extensibility.corexunit.v3.mtp-v1, xunit.v3.mtp-v2, xunit.v3.core.mtp-v1, xunit.v3.core.mtp-v2YTest.MTP.XUnit2 (xUnit v2 MTP shim)Xunit.SkippableFact, Xunit.Combinatorial, Xunit.StaFact (see Step 10)Add MSTest v4. Two options -- both correct.
Option A -- MSTest metapackage (recommended for incremental migrations):
<ItemGroup>
<PackageReference Include="MSTest" Version="4.1.0" />
</ItemGroup>
The MSTest metapackage pulls in MSTest.TestFramework, MSTest.TestAdapter, MSTest.Analyzers, and Microsoft.NET.Test.Sdk -- so VSTest discovery (vstest.console, classic dotnet test) still works.
> MTP code-coverage caveat for Option A: Microsoft.NET.Test.Sdk pulls VSTest's Microsoft.CodeCoverage transitively. If the project from Step 1 is on MTP and uses code coverage, that transitive dependency can interfere with MTP's collector (Microsoft.Testing.Extensions.CodeCoverage). Prefer Option B (MSTest.Sdk without UseVSTest) for MTP projects -- the SDK omits Microsoft.NET.Test.Sdk and wires the MTP coverage collector instead. If you must stay on Option A for an MTP project, verify coverage works on a representative test run before merging.
Option B -- MSTest.Sdk:
<Project Sdk="MSTest.Sdk/4.1.0">
<PropertyGroup>
<!-- Keep the project's existing TargetFramework -- do NOT retarget during migration. -->
<TargetFramework>$(ExistingTargetFramework)</TargetFramework>
</PropertyGroup>
</Project>
MSTest.Sdk defaults to MTP. To preserve a VSTest project, opt back in with <UseVSTest>true</UseVSTest> -- the SDK then pulls in Microsoft.NET.Test.Sdk automatically (no extra PackageReference needed):
<PropertyGroup>
<UseVSTest>true</UseVSTest>
</PropertyGroup>
For solutions with several test projects, prefer pinning the MSTest.Sdk version in global.json so it lives in one place:
{
"msbuild-sdks": {
"MSTest.Sdk": "4.1.0"
}
}
With the pin in global.json, the project line simplifies to <Project Sdk="MSTest.Sdk">.
When switching to MSTest.Sdk, also remove now-redundant properties: <OutputType>Exe</OutputType>, <IsPackable>false</IsPackable>, <IsTestProject>true</IsTestProject>, <EnableMSTestRunner>.
MSTest.Sdk without UseVSTest=true silently flips a VSTest project to MTP. Add <UseVSTest>true</UseVSTest> to the project (the SDK pulls in Microsoft.NET.Test.Sdk automatically -- no manual PackageReference needed).<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner> only affects the dotnet run entry point and is not a runner switch in Test Explorer or dotnet test. Do not infer the platform from this property in either direction -- defer to the platform-detection skill (see Step 1).xunit.runner.json and port any settings you need (parallelization, [CollectionBehavior], appDomain) per Step 11's "xunit.runner.json -> MSTest" sub-table. The settings have no direct MSBuild-property mapping.using Xunit; and using Xunit.Abstractions; from C# files (the rewriter will add using Microsoft.VisualStudio.TestTools.UnitTesting; instead in Step 4).Apply these rewrites to every C# test file. Class-level first, then method-level.
Class:
[TestClass] to every class that contained xUnit [Fact]/[Theory] methods (xUnit had no class-level requirement).sealed would break that pattern. Sealing is an optional follow-up handled by writing-mstest-tests, not part of the mechanical migration.using Xunit; / using Xunit.Abstractions; with using Microsoft.VisualStudio.TestTools.UnitTesting;.Methods:
> [Ignore] and [Timeout] are modifiers, not discovery attributes. Always emit [TestMethod] *alongside* them -- a method with [Ignore] but no [TestMethod] is silently skipped by the test runner (no error, no skip count). Same for [Timeout].
| xUnit | MSTest |
|---|---|
| [Fact] | [TestMethod] |
| [Theory] | [TestMethod] (parameterized; MSTest 3+ no longer needs [DataTestMethod]) |
| [Fact(DisplayName = "x")] | [TestMethod("x")] (v3 of MSTest) or [TestMethod(DisplayName = "x")] (v4) |
| [Fact(Skip = "reason")] | [TestMethod] + [Ignore("reason")] (both attributes required) |
| [Fact(Timeout = 5000)] | [TestMethod] + [Timeout(5000)] (both attributes required) |
| [Trait("Category", "Unit")] | [TestCategory("Unit")] |
| [Trait("Owner", "alice")] | [TestProperty("Owner", "alice")] |
> Both [TestCategory] and [TestProperty] are filterable at runtime (--filter "TestCategory=Unit" / --filter "Owner=alice"). [TestCategory] targets Assembly, Class, and Method, so an xUnit [assembly: Trait("Category", ...)] keeps its assembly scope under MSTest as [assembly: TestCategory(...)]. [TestProperty] targets only Class and Method — there is no AttributeTargets.Assembly, so an assembly-level xUnit trait with an arbitrary key must collapse to [assembly: TestCategory(...)] (or be pushed down to every class). Use [TestCategory] for the conventional category trait; use [TestProperty] for arbitrary key/value metadata at class/method scope. For environmental skips (OS-specific, CI-only), MSTest 3.10+'s [OSCondition] / [CICondition] are usually a better fit than overloading a trait -- see Step 6 / cheatsheet §3.9.
| xUnit | MSTest |
|---|---|
| [InlineData(1, 2)] | [DataRow(1, 2)] |
| [InlineData(1, DisplayName = "case 1")] | [DataRow(1, DisplayName = "case 1")] |
| [MemberData(nameof(Cases))] returning IEnumerable<object[]> | [DynamicData(nameof(Cases))] returning IEnumerable<object[]> |
| [MemberData(nameof(Cases), MemberType = typeof(X))] | [DynamicData(nameof(Cases), typeof(X))] |
| [MemberData(nameof(Method), arg1, arg2)] (parameterized member) | Manual: convert to a parameterless property or compute the inputs inside the test |
| [ClassData(typeof(MyData))] (class implementing IEnumerable<object[]>) | Add a static property => new MyData() on the test class, then [DynamicData(nameof(Cases))] |
| TheoryData<int, string> | IEnumerable<object[]>, IEnumerable<(int, string)> (MSTest 3.7+ ValueTuple), or IEnumerable<TestDataRow<(int, string)>> (strongly-typed with per-row metadata) |
| Custom DataAttribute subclass | Manual: implement ITestDataSource (GetData, GetDisplayName) |
Prefer ValueTuple data sources for new MSTest tests (see writing-mstest-tests), but for migration keep IEnumerable<object[]> -- it minimizes diff churn and works in both MSTest 3 and 4.
Most common cases inline. For the full table including string/collection/type/numeric and event/equivalence assertions, see references/mapping-cheatsheet.md §3.
| xUnit | MSTest |
|---|---|
| Assert.Equal(expected, actual) | Assert.AreEqual(expected, actual) |
| Assert.NotEqual(a, b) | Assert.AreNotEqual(a, b) |
| Assert.True(x) / Assert.False(x) | Assert.IsTrue(x) / Assert.IsFalse(x) |
| Assert.Null(x) / Assert.NotNull(x) | Assert.IsNull(x) / Assert.IsNotNull(x) |
| Assert.Same(a, b) / Assert.NotSame(a, b) | Assert.AreSame(a, b) / Assert.AreNotSame(a, b) |
| Assert.Throws<T>(() => ...) | Assert.ThrowsExactly<T>(() => ...) (see trap below) |
| Assert.ThrowsAny<T>(() => ...) | Assert.Throws<T>(() => ...) |
| await Assert.ThrowsAsync<T>(...) | await Assert.ThrowsExactlyAsync<T>(...) |
| Assert.IsType<T>(x) / Assert.IsAssignableFrom<T>(x) | Assert.IsInstanceOfType<T>(x) (MSTest v4 returns the typed value) |
| Assert.Empty(coll) / Assert.NotEmpty(coll) | Assert.IsEmpty(coll) / Assert.IsNotEmpty(coll) |
| Assert.Single(coll) | var item = Assert.ContainsSingle(coll); |
| Assert.Contains(item, coll) / Assert.DoesNotContain(...) | Same -- Assert.Contains / Assert.DoesNotContain |
| Assert.Contains("sub", str) / StartsWith / EndsWith / Matches | Same (MSTest 3.8+) or StringAssert.* |
| Assert.Skip("reason") (v3 runtime) | Assert.Inconclusive("reason") |
| Assert.SkipWhen(cond, "reason") (v3) | If cond is environmental: [OSCondition] / [CICondition] (MSTest 3.10+); otherwise if (cond) Assert.Inconclusive("reason"); |
| Assert.SkipUnless(cond, "reason") (v3) | Same -- prefer a condition attribute when the predicate is environmental; otherwise if (!cond) Assert.Inconclusive("reason"); |
Critical semantic trap -- exception assertions:
Assert.Throws<T> = exact type match -> MSTest Assert.ThrowsExactly<T>.Assert.ThrowsAny<T> = derived types also match -> MSTest Assert.Throws<T>.Reversing these flips the assertion semantics silently. Verify by name, not by visual similarity.
No-equivalent assertions -- convert manually (see cheatsheet §3.11):
Assert.Collection(items, e1 => ..., e2 => ...) -> assert count, then per-elementAssert.All(items, x => ...) -> foreachAssert.Equivalent(expected, actual) -> deep equality manually, or a third-party libraryAssert.Raises<T> / Assert.PropertyChanged -> manual event subscription + flag checkRecord.Exception / Record.ExceptionAsync -> try/catch returning the exception (or Assert.ThrowsExactly<T> if you know the type)Constructor / IDisposable / IAsyncDisposable / IAsyncLifetime:
| xUnit | MSTest |
|---|---|
| Constructor (sync setup) | Keep constructor (MSTest also instantiates per test). Drop xUnit-only ITestOutputHelper param -- see Step 9 |
| Dispose() (sync teardown) | Keep Dispose() (MSTest supports IDisposable) or rewrite as [TestCleanup] public void Cleanup() { ... } |
| DisposeAsync() (async teardown) | Keep IAsyncDisposable.DisposeAsync() or rewrite as [TestCleanup] public async Task CleanupAsync() { ... } |
| IAsyncLifetime.InitializeAsync | [TestInitialize] public async Task InitAsync() { ... } |
| IAsyncLifetime.DisposeAsync | [TestCleanup] public async Task CleanupAsync() { ... } |
> Per writing-mstest-tests: prefer the constructor for sync init (it allows readonly fields). Use [TestInitialize] only for async setup or when you need TestContext.
IClassFixture<T> -- class-level shared state (mechanical):
// xUnit v2/v3
public class DbFixture : IDisposable
{
public string ConnectionString { get; } = "...";
public void Dispose() { /* cleanup */ }
}
public class OrderTests : IClassFixture<DbFixture>
{
private readonly DbFixture _fixture;
public OrderTests(DbFixture fixture) => _fixture = fixture;
}
// MSTest equivalent
[TestClass]
public sealed class OrderTests
{
private static DbFixture? s_fixture;
[ClassInitialize]
public static void ClassInit(TestContext context) => s_fixture = new DbFixture();
[ClassCleanup]
public static void ClassCleanup() => s_fixture?.Dispose();
}
ICollectionFixture<T> / [CollectionDefinition] -- shared by tests in the same collection (judgement call):
xUnit collections do two things simultaneously: (1) share a fixture instance across multiple test classes, and (2) serialize those classes (no parallel execution within a collection). MSTest does not have a built-in equivalent that preserves both semantics. Pick one -- do not silently map to [AssemblyInitialize]:
[ClassInitialize], OR introduce a static Lazy<T> shared helper. Add [DoNotParallelize] on each class to preserve serialization.[AssemblyInitialize] / [AssemblyCleanup] in a dedicated AssemblySetup class and confirm with the user that widening the scope is acceptable. Note that this changes parallelization semantics.> REQUIRED -- communicate the scope decision before applying it. Silently widening fixture scope across the assembly is the most common way this migration regresses tests. Use this template (replace bracketed text):
>
> "The xUnit [Collection(\"<name>\")] shared a <Fixture> between \<N\> classes and serialized them. I am mapping that to: a static Lazy<<Fixture>> shared by each class's [ClassInitialize] (scope: per-class, shared via static -- not widened to assembly), plus [DoNotParallelize] on <ClassA> and <ClassB> to preserve the serialization. The alternative -- [AssemblyInitialize] -- would widen the fixture to every test in the assembly, which I rejected because \<reason\>."
ITestOutputHelper -> TestContext:
// xUnit (v2 and v3)
public class MyTests
{
private readonly ITestOutputHelper _output;
public MyTests(ITestOutputHelper output) => _output = output;
[Fact]
public void Test() => _output.WriteLine("...");
}
// MSTest (v3.6+ supports TestContext in constructor)
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext) => _testContext = testContext;
[TestMethod]
public void Test() => _testContext.WriteLine("...");
}
If the project pins MSTest < 3.6 (rare after Step 2), use property injection instead:
public TestContext TestContext { get; set; } = null!;
xUnit v3 TestContext.Current (TestContext.Current is static in xUnit v3; in MSTest you must use the instance TestContext obtained via the same constructor or property injection shown above):
TestContext.Current.CancellationToken -> _testContext.CancellationToken (MSTest 3.6+)TestContext.Current.AddAttachment(name, path) -> _testContext.AddResultFile(path)TestContext.Current.TestOutputHelper.WriteLine(...) -> _testContext.WriteLine(...)> REQUIRED for CancellationToken: Add the constructor injection from above even if the class only uses TestContext.Current.CancellationToken (no ITestOutputHelper). Do NOT replace TestContext.Current.CancellationToken with a new CancellationTokenSource -- that loses the test-host's cancellation linkage and changes behavior under timeouts.
// xUnit v3
[Fact]
public async Task WorkRespectsCancellation()
{
var ct = TestContext.Current.CancellationToken;
await Task.Delay(1, ct);
Assert.False(ct.IsCancellationRequested);
}
// MSTest (note: Assert.False -> Assert.IsFalse from Step 6)
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext) => _testContext = testContext;
[TestMethod]
public async Task WorkRespectsCancellation()
{
var ct = _testContext.CancellationToken;
await Task.Delay(1, ct);
Assert.IsFalse(ct.IsCancellationRequested);
}
}
| xUnit companion | MSTest equivalent |
|---|---|
| Xunit.SkippableFact ([SkippableFact], Skip.If, Skip.IfNot) | For environmental predicates (OS/CI/arch): MSTest 3.10+ condition attributes ([OSCondition], [CICondition], etc.). Otherwise: [Ignore] (compile-time) or Assert.Inconclusive("reason") (runtime). Remove the package |
| Xunit.Combinatorial (CombinatorialData], [CombinatorialValues]) | [Combinatorial.MSTest (community port; attribute surface matches xUnit.Combinatorial). Or expand combinations into explicit [DataRow]s / [DynamicData] |
| Xunit.StaFact ([StaFact], [WpfFact]) | [TestMethod] + manual STA thread. No MSTest equivalent for [WpfFact]; flag for manual conversion |
| Verify.Xunit | Verify.MSTest -- swap the package; usage is similar |
| FluentAssertions / Shouldly / AwesomeAssertions | Keep -- assertion library is framework-agnostic |
| Moq / NSubstitute / FakeItEasy | Keep -- mocking library is framework-agnostic |
> This is the most common source of post-migration regressions. xUnit and MSTest have opposite defaults. Do not skip this step even if Step 1 said tests passed cleanly.
| Framework | Across test classes | Within a test class | Test-class instance lifetime |
|---|---|---|---|
| xUnit v2 | Parallel (one class per worker thread) | Serial (one test method at a time) | New instance per test method |
| xUnit v3 | Parallel (same as v2) | Serial (same as v2) | New instance per test method |
| MSTest (default) | Serial (one class at a time) | Serial (one test method at a time) | New instance per test method |
| MSTest + [assembly: Parallelize(Scope = ClassLevel)] | Parallel | Serial | Same |
| MSTest + [assembly: Parallelize(Scope = MethodLevel)] | Parallel | Parallel -- more aggressive than xUnit | Same |
Workers = 0 means "use all available logical cores" (MSTest's recommended default for parallel runs); any positive integer caps the worker count.
Choice A -- Match xUnit's behaviour exactly (recommended default):
// Place in any .cs file at assembly scope (often AssemblyInfo.cs or GlobalUsings.cs)
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
Use this when the suite was healthy on xUnit and you want zero behavioural change. It preserves "parallel across classes, serial within a class" exactly.
> REQUIRED -- explicitly tell the user why this attribute is needed. When applying Choice A, include this sentence (verbatim or near-verbatim) in your final summary:
>
> "MSTest defaults to serial execution across classes (unlike xUnit, which parallelizes classes by default), so this [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)] is required to match the project's previous xUnit parallel-class behaviour. Without it, the suite would still pass but run roughly one-class-at-a-time and CI throughput would drop."
>
> The user must understand this is opt-in under MSTest -- a silent omission looks like a no-op but is actually a behavioural regression.
Choice B -- Adopt MSTest's serial default:
// No [assembly: Parallelize] needed -- this is the default
Use this only when the suite has known shared-state issues (Step 1.6) that you intend to leave unfixed for now, or when wall-clock time is not a concern. Expect significantly slower CI.
Choice C -- Selective parallelization:
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
Plus per-class opt-out for the classes that genuinely cannot run concurrently:
[TestClass]
[DoNotParallelize]
public sealed class DatabaseIntegrationTests { /* ... */ }
Use this when most of the suite is isolated but a few classes touch shared state (one DB, fixed ports, file system locations). This is usually the right answer when migrating from xUnit collections.
> Do not pick ExecutionScope.MethodLevel to "match xUnit" -- it parallelizes test methods *within* a single class, which xUnit never does. It is more aggressive than xUnit and will surface latent intra-class state issues.
| xUnit pattern | MSTest equivalent |
|---|---|
| [assembly: CollectionBehavior(DisableTestParallelization = true)] | Omit [assembly: Parallelize] (or use Choice B above) |
| [assembly: CollectionBehavior(MaxParallelThreads = N)] | [assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)] |
| [Collection("Db")] on multiple classes (forces those classes to share a fixture and run serially) | [DoNotParallelize] on each of those classes (preserves serialization) + Step 8 fixture handling (preserves sharing) |
| [CollectionDefinition("Db", DisableParallelization = true)] | Same as above -- [DoNotParallelize] on each member class |
| [Collection("Foo")] used only for fixture sharing (no parallelization concern) | Step 8 fixture handling; do not add [DoNotParallelize] |
The distinction in the last two rows matters: xUnit collections conflate "share state" with "serialize". MSTest decouples them. Read the original [CollectionDefinition] carefully -- if DisableParallelization is false (or omitted), only the fixture sharing semantic needs to migrate, not the serialization.
If pass/fail counts diverge from the baseline after migration, parallelization is the first place to look:
[DoNotParallelize] to the offending classes, or fix the shared state.[assembly: Parallelize]. Add Choice A.MethodLevel instead of ClassLevel. Switch to ClassLevel.xunit.runner.json migrationDelete xunit.runner.json. Port relevant settings:
| xunit.runner.json | MSTest equivalent |
|---|---|
| "parallelizeAssembly": false | Default in MSTest -- no action |
| "parallelizeTestCollections": false | Omit [assembly: Parallelize] (Choice B) |
| "maxParallelThreads": N | [assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)] |
| "methodDisplay": "method" / "classAndMethod" | No equivalent (MSTest always uses class + method) |
| "diagnosticMessages": true | Use --diagnostic on the CLI, or set verbosity in .runsettings |
| "preEnumerateTheories": false | No equivalent (MSTest enumerates [DataRow]/[DynamicData] eagerly) |
| "longRunningTestSeconds": N | Use [Timeout(N * 1000)] per test |
| "appDomain": "denied" / "ifAvailable" | No equivalent (MSTest uses no app domains on modern .NET) |
If the project uses xUnit traits in CI filter expressions (e.g., --filter "Category=Unit" with xUnit), the equivalent MSTest filter is --filter "TestCategory=Unit" (VSTest) or --filter-trait "TestCategory=Unit" (MTP). Update CI pipelines accordingly.
Some xUnit assembly attributes have direct MSTest equivalents at assembly scope; others must be removed (and re-applied per class/method) or reimplemented against MSTest extensibility.
Convert (assembly scope preserved):
[assembly: Xunit.Trait("Category", "v")] -> [assembly: TestCategory("v")] -- TestCategoryAttribute targets Assembly, Class, and Method; assembly application propagates to every test.Convert (assembly scope NOT preserved):
[assembly: Xunit.Trait("k", "v")] (non-category key) -> collapse to [assembly: TestCategory("v")] if the value alone is sufficient as a filter, or move the trait down to every test class as [TestProperty("k", "v")]. TestPropertyAttribute only targets Class and Method (no AttributeTargets.Assembly) -- [assembly: TestProperty(...)] will not compile.Delete (no MSTest equivalent or now handled elsewhere):
[assembly: CollectionBehavior(...)] -- replaced by [assembly: Parallelize(...)] (Step 11)[assembly: TestCaseOrderer(...)] -- reimplement against MSTest extensibility; flag for manual conversion[assembly: TestCollectionOrderer(...)] -- flag for manual conversion[assembly: TestFramework(...)][assembly: CaptureConsole] (xUnit v3) -- MSTest does not capture console by defaultCustom orderers/test framework hooks must be reimplemented against MSTest's extensibility model (TestMethodAttribute subclasses, ITestDataSource, etc.) -- stop and flag for manual conversion if present.
dotnet build -- must succeed with zero errors. Address remaining errors using the mapping reference.dotnet test -- run with the same filter/runner combination as before migration.[DoNotParallelize] to the specific class(es), or fix the shared state.[assembly: Parallelize]. See Step 11 Choice A.[Ignore]) that used to run via Assert.SkipWhen? Convert to runtime Assert.Inconclusive if you want them to execute when the condition is false.[DataRow] literal types (1 int vs 1L long -- MSTest enforces exact match unlike xUnit).Assert.Collection or Assert.All was dropped -- restore manually.test-anti-patterns, assertion-quality) to identify follow-up improvements -- e.g., replacing Assert.IsTrue(x.Count() == 3) with Assert.HasCount(3, x).xunit*, xunit.v3.*, or YTest.MTP.XUnit2 package references remain[TestClass] and every test method has [TestMethod]using Xunit; and using Xunit.Abstractions; removedxunit.runner.json removed; equivalent config in .runsettings / [assembly: Parallelize][assembly: Parallelize(...)] is present (Choice A/C, matches xUnit default) or the user accepted the serial default (Choice B). Not left unspecified by accidentTargetFramework unchanged unless MSTest v4 forced an upgrade (and the user approved)| Pitfall | Symptom | Fix |
|---|---|---|
| Leaving parallelization unspecified | Suite that ran in 30s on xUnit now takes minutes on MSTest; or new flakiness from inherited xUnit assumptions | Pick a target parallelization model explicitly in Step 11 (Choice A matches xUnit) -- do not leave it as the MSTest serial default by accident |
| Picking ExecutionScope.MethodLevel to "match xUnit" | New flakiness on tests sharing instance state within a class | Use ExecutionScope.ClassLevel -- it matches xUnit exactly |
| Mapping Assert.Throws<T> to Assert.Throws<T> | Tests pass for derived exception types they shouldn't | Map xUnit Assert.Throws<T> to MSTest Assert.ThrowsExactly<T> |
| Silently widening ICollectionFixture to assembly scope | State leak between unrelated tests; new flakiness | Step 8 -- pick scope explicitly and disclose to the user |
| MSTest.Sdk flipping VSTest project to MTP | vstest.console finds zero tests; CI breaks | Add <UseVSTest>true</UseVSTest> (no separate Microsoft.NET.Test.Sdk package needed -- the SDK pulls it in) |
| [DataRow] type mismatch | Theory cases compile in xUnit but produce MSTest runtime errors | Use exact literal types: 1 int, 1L long, 1.0f float |
| Assert.SkipUnless becomes [Ignore] | Tests that *would* have run on this machine now silently skip everywhere | Use a condition attribute ([OSCondition]/[CICondition], MSTest 3.10+) when the predicate is environmental; otherwise runtime Assert.Inconclusive |
| Dropping Assert.Collection / Assert.All without replacement | Test passes but verifies nothing | Restore as explicit foreach + per-element assertions |
| Leaving xunit.runner.json in the project | Build warning + dead config | Delete the file after porting settings |
After this migration:
migrate-vstest-to-mtp if you want to move to Microsoft.Testing.Platform (separate, committable migration).writing-mstest-tests to polish converted code: replace Assert.IsTrue(x.Count() == 3) with Assert.HasCount(3, x), prefer ValueTuple data sources, mark classes sealed, etc.test-anti-patterns / assertion-quality to catch any quality regressions introduced by mechanical conversion.Take microsoft/migrate-xunit-to-mstest from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.