migrate-xunit-to-mstest
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. D
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-test-migration/skills/migrate-xunit-to-mstest
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
xUnit -> MSTest Migration
Convert xUnit.net v2 or v3 tests to MSTest v4 without changing the target framework or test platform. A successful migration builds, discovers the same tests, and preserves pass/fail results and execution semantics.
Scope
Use this skill only when the project contains xUnit packages or source and the user wants MSTest. If the project already uses MSTest and contains no xUnit tests, report that no framework migration is needed and make no changes.
Do not combine this framework conversion with a target-framework upgrade or VSTest/MTP migration. Complete and verify one migration before starting another.
Workspace Contract
- Continue after skill activation. Search the current working directory for the staged project and source; never look for user files under this skill's base directory.
- Open the literal paths returned by glob/search. If one reader or patch tool rejects a path that was just found, retry with another available tool. Do not ask the user for a path until current-workspace discovery is exhausted.
- Classify by the requested deliverable: "convert this project" means edit, build, and test; "give me a plan" or "how would I convert it?" means answer. Do not replace execution with "please provide the files" when files are present.
- The final response must state the source xUnit version, preserved runner, changed files, each high-risk semantic mapping applied, and actual test counts. Assertions about fixture lifetime, Owner mapping, cancellation, or parallelization must be visible in the resulting source, not only prose.
Response Mode
- Full migration request: inspect the project, make the edits, build, and run tests. Do not stop after giving a plan.
- Focused compile error or API question: inspect the relevant code and apply only that mapping. Do not narrate the entire workflow.
- Unsupported target framework: stop before changing packages. MSTest v4 requires .NET 8+ or .NET Framework 4.6.2+ for test applications; offer a separately approved TFM upgrade or MSTest v3 as the intermediate target.
Decisions That Change the Result
Apply these before the mechanical mapping:
| Detected state | Required action |
|---|---|
| No xUnit package, namespace, attribute, or fixture remains | Stop. Make no file changes, report that migration is unnecessary, and run the existing dotnet test command once to prove the already-MSTest project is healthy. |
| Source uses VSTest | Keep the existing VSTest property/configuration. Prefer retaining and updating a source project's explicit Microsoft.NET.Test.Sdk pin; a repository that intentionally relies on the MSTest metapackage's transitive dependency may keep that convention. Do not introduce MTP properties. |
| Source uses MTP | Replace xUnit-specific MTP selection with MSTest MTP configuration. Prefer MSTest.Sdk; with the metapackage, set EnableMSTestRunner=true and OutputType=Exe. Preserve native-versus-bridged command integration, and do not add <UseVSTest>true</UseVSTest> or other VSTest-only configuration. |
| Source relies on xUnit's default parallelization | Add [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)] to a compiled .cs file when the current project has at least two independently runnable test classes. In a one-class project with no explicit parallel setting, omit it because class-level concurrency is not observable. Translate explicit CollectionBehavior or xunit.runner.json settings regardless of current class count. Before reporting completion, read the changed file back and name it in the result. |
For detailed mappings and examples, search references/mapping-cheatsheet.md for constructs actually present in the project and read only the matching sections. Do not load or reproduce the whole reference.
Fast Path
For a routine project migration, converge in four phases: one batched discovery read/search, one edit pass, one dotnet test, and one concise result. Do not:
- list a directory and then reread the same files through another tool
- copy project files into the loaded skill or its
references/directory; those files are read-only guidance, not an editing workspace - try
dotnet test --no-restoreunless restore is already known to be current - run separate restore, build, and test commands when
dotnet testis sufficient - rerun a passing test command or inspect unchanged files for confirmation
Use an existing CI/test result as the parity baseline when available. Run a new pre-edit baseline only when counts are unavailable and the migration contains data-driven tests, fixtures, skips, custom extensions, shared state, or other behavior whose parity cannot be established from source alone.
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:
xunit2.x and related packages -> xUnit v2xunit.v3orxunit.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, customFactAttribute/TheoryAttribute/DataAttributeAssert.Throws,ThrowsAny,IsType,Record.Exception, event assertionsITestOutputHelper,TestContext.Current,IAsyncLifetimeCollectionBehavior,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 to the genericAssert.IsExactInstanceOfType<T>;Assert.IsAssignableFrom<T>maps to the genericAssert.IsInstanceOfType<T>. When the xUnit assertion's typed return value is assigned, preserve that assignment and use the generic MSTest overload rather than a non-genericTypeoverload. - 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]. IClassFixture<T>means one fixture instance per test class, shared by all methods in that class. Map it to a staticTfield created once by a static[ClassInitialize]method that acceptsTestContext, and dispose it once from static[ClassCleanup]. Never use[TestInitialize]/[TestCleanup]for this mapping because that creates one fixture per test method.- 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 constructor-injected or property-based MSTestTestContext, and replace each_output.WriteLine(...)call with the correspondingTestContext.WriteLine(...). In the final result, name both theTestContextinjection/property and theWriteLinemapping explicitly; "migrated output" is not enough to demonstrate parity.
xUnit runs classes in parallel by default; MSTest runs them serially. When the current project has two or more independently runnable test classes, preserve that effective behavior with:
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
For a one-class project with no explicit xUnit parallel setting, do not add an assembly policy: there is no class-level concurrency to preserve. Always translate explicit CollectionBehavior or xunit.runner.json settings. 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, orDataRowliteral 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
- missing cases -> discovery attributes,
- Confirm no xUnit package, namespace, attribute, runner configuration, or fixture interface remains unless explicitly documented for manual follow-up.
- After the final passing test, read back each changed file that implements a high-risk mapping, plus any runner or parallelization configuration. In the result, name the file and the exact target APIs that implement its lifecycle, data, skip, output, assertion, fixture-scope, or parallelization behavior. Preserve type arguments and member names such as
[ClassInitialize],TestContext.WriteLine, andAssert.IsExactInstanceOfType<T>; generic claims such as "converted attributes" or "migrated output" are not evidence of parity.
Keep the final response concise and outcome-focused:
- Changed: name the files and exact high-risk mappings applied.
- Verified: give the final test command and discovered/passed/failed/skipped counts.
- Preserved: state the unchanged target framework and test platform, plus any fixture or parallelization scope decision.
- Remaining: identify manual follow-up, or say none.
Completion Criteria
- Current xUnit version and test platform were identified
- xUnit packages and source constructs were converted
- Target framework and test platform stayed unchanged
- Fixture scope and effective parallelization decisions are explicit, including justified omission when concurrency is not observable
- Build succeeds
- Test discovery and result counts match the baseline
- Any unsupported custom extension point is called out rather than approximated
Follow-up
Run migrate-vstest-to-mtp separately if the user also wants MTP. Use writing-mstest-tests only after parity is established to polish the converted MSTest code.
Files (skills)
-
references
-
mapping-cheatsheet.md 26.4 KB
# xUnit -> MSTest Mapping Cheatsheet Comprehensive reference loaded by the `migrate-xunit-to-mstest` skill. Look up specific xUnit constructs and their MSTest v4 equivalents, including edge cases and "no equivalent -- manual" calls. Target framework throughout: **MSTest v4** (the few v3-only spellings are explicitly marked). ## Table of contents - [1. Test discovery (class + method attributes)](#1-test-discovery-class--method-attributes) - [2. Data-driven tests](#2-data-driven-tests) - [3. Assertions](#3-assertions) - [3.1 Equality, null, reference](#31-equality-null-reference) - [3.2 Boolean](#32-boolean) - [3.3 Type checks](#33-type-checks) - [3.4 Numeric / comparison](#34-numeric--comparison) - [3.5 String](#35-string) - [3.6 Collection](#36-collection) - [3.7 Exceptions](#37-exceptions) - [3.8 Async exception assertions](#38-async-exception-assertions) - [3.9 Skip / inconclusive](#39-skip--inconclusive) - [3.10 Fail](#310-fail) - [3.11 No-equivalent assertions](#311-no-equivalent-assertions) - [4. Fixtures and lifecycle](#4-fixtures-and-lifecycle) - [5. Output / TestContext](#5-output--testcontext) - [6. Cancellation and timeouts (xUnit v3 specifics)](#6-cancellation-and-timeouts-xunit-v3-specifics) - [7. Parallelization](#7-parallelization) - [8. Assembly-level attributes](#8-assembly-level-attributes) - [9. Packages](#9-packages) - [10. Companion / extension libraries](#10-companion--extension-libraries) ## 1. Test discovery (class + method attributes) | xUnit | MSTest | |---|---| | *(no class attribute)* | `[TestClass]` (required) | | *(no class modifier)* | Preserve the original hierarchy. Do **not** add `sealed` mechanically -- base/derived test classes are common in xUnit and sealing would break them. `writing-mstest-tests` can apply `sealed` as a follow-up where appropriate. | | `[Fact]` | `[TestMethod]` | | `[Theory]` | `[TestMethod]` (MSTest 3+ unified; `[DataTestMethod]` still works but is not needed) | | `[Fact(DisplayName = "x")]` | MSTest 4: `[TestMethod(DisplayName = "x")]`; MSTest 3: `[TestMethod("x")]` | | `[Theory(DisplayName = "x")]` | Same as above on the `[TestMethod]` | | `[Fact(Skip = "reason")]` | `[TestMethod]` + `[Ignore("reason")]` (the `[Ignore]` attribute alone does not discover a test -- you still need `[TestMethod]`) | | `[Fact(Timeout = 5000)]` | `[TestMethod]` + `[Timeout(5000)]` (same -- `[Timeout]` is a modifier, not a discovery attribute) | | `[Trait("Category", "Unit")]` | `[TestCategory("Unit")]` | | `[Trait("Owner", "alice")]` | `[Owner("alice")]` (`Owner` is a reserved VSTest property and is rejected by `TestPropertyAttribute`) | | `[Collection("Db")]` | Step 8 + Step 11: `[DoNotParallelize]` (serialization) + `[ClassInitialize]` (sharing) -- preserve scope explicitly | | Custom `FactAttribute` subclass | Custom `TestMethodAttribute` subclass overriding `ExecuteAsync` (MSTest v4). See `writing-mstest-tests` and `migrate-mstest-v3-to-v4` for `CallerInfo` constructor pattern | | Custom `TheoryAttribute` subclass | Same -- subclass `TestMethodAttribute`; expose data via `ITestDataSource` | > Both `[TestCategory]` and `[TestProperty]` are **filterable** at runtime: > - `[TestCategory("Unit")]` -> `--filter "TestCategory=Unit"` (VSTest) / `--filter-trait "TestCategory=Unit"` (MTP); targets `Assembly`, `Class`, and `Method` > - `[Owner("alice")]` -> `--filter "Owner=alice"` (VSTest) / `--filter-trait "Owner=alice"` (MTP) > - `[TestProperty("Key", "Value")]` -> `--filter "Key=Value"` (VSTest) / `--filter-trait "Key=Value"` (MTP); targets `Class` and `Method` only (no `AttributeTargets.Assembly`) > > Use `[TestCategory]` for the conventional category trait, `[Owner]` for the reserved owner property, and `[TestProperty]` for other key/value metadata at class/method scope. An `[assembly: Trait("Category", ...)]` in xUnit can be migrated to `[assembly: TestCategory(...)]`. An assembly-level `[Trait]` with an arbitrary key cannot map to `[assembly: TestProperty(...)]` -- collapse it to `[assembly: TestCategory(...)]` or move it down to every class (see Section 8). > > **Conditional skips** (xUnit `[Trait("OS", "Windows")]` patterns that gate execution): MSTest 3.10+ offers dedicated condition attributes -- `[OSCondition]` and `[CICondition]` -- which are usually a better fit than overloading `[TestCategory]` for environmental gating. (There is no `ArchitectureCondition` or `NonParallelizableCondition` attribute in MSTest; for non-parallel intent use `[DoNotParallelize]`, and for architecture gating fall back to `if (RuntimeInformation.OSArchitecture != ...) Assert.Inconclusive(...)`.) See Section 3.9. ## 2. Data-driven tests | xUnit | MSTest | |---|---| | `[InlineData(1, 2)]` | `[DataRow(1, 2)]` | | `[InlineData(1, DisplayName = "case 1")]` | `[DataRow(1, DisplayName = "case 1")]` | | `[InlineData(null)]` | `[DataRow(null)]` | | `[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(Cases))]` returning `TheoryData<int, string>` | `[DynamicData(nameof(Cases))]` returning `IEnumerable<object[]>`, `IEnumerable<(int, string)>` (MSTest 3.7+ ValueTuple), or `IEnumerable<TestDataRow<(int, string)>>` (strongly-typed with per-row `DisplayName`/`Ignore` metadata -- see [docs](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-mstest-writing-tests-data-driven#supported-data-source-types)) | | `[MemberData(nameof(Method), arg1, arg2)]` (parameterized member) | **Manual** -- convert to a parameterless property/method, or move parameter logic into the test method | | `[ClassData(typeof(MyData))]` where `MyData : IEnumerable<object[]>` | Expose a static `IEnumerable<object[]> Cases => new MyData();` and use `[DynamicData(nameof(Cases))]` | | `[ClassData(typeof(MyData))]` where `MyData : TheoryData<...>` | Same approach; convert `TheoryData<...>` to `IEnumerable<object[]>` or ValueTuples | | Custom `DataAttribute` subclass | **Manual** -- implement `ITestDataSource` (`GetData` + `GetDisplayName`) | **Literal-type trap.** MSTest's `[DataRow]` enforces exact type matching against method parameters. xUnit's `[InlineData]` is more permissive. After conversion, audit literals: | Parameter type | Required literal | |---|---| | `int` | `1`, `0`, `-1` | | `long` | `1L` | | `float` | `1.0f` | | `double` | `1.0` or `1.0d` | | `decimal` | `1.0m` | | `uint` | `1U` | | `Type` | `typeof(...)` | ## 3. Assertions ### 3.1 Equality, null, reference | xUnit | MSTest | |---|---| | `Assert.Equal(expected, actual)` | `Assert.AreEqual(expected, actual)` | | `Assert.Equal(expected, actual, comparer)` | `Assert.AreEqual(expected, actual, comparer)` | | `Assert.Equal(0.1, 0.10001, 3)` (precision) | `Assert.AreEqual(0.1, 0.10001, delta: 0.001)` | | `Assert.Equal("a", "A", ignoreCase: true)` | `Assert.AreEqual("a", "A", ignoreCase: true)` | | `Assert.NotEqual(a, b)` | `Assert.AreNotEqual(a, b)` | | `Assert.Same(a, b)` | `Assert.AreSame(a, b)` | | `Assert.NotSame(a, b)` | `Assert.AreNotSame(a, b)` | | `Assert.Null(x)` | `Assert.IsNull(x)` | | `Assert.NotNull(x)` | `Assert.IsNotNull(x)` | | `Assert.Equivalent(expected, actual)` | **Manual** -- no built-in deep-equality assertion. Use a third-party library (FluentAssertions `.Should().BeEquivalentTo(...)`) or write member-by-member assertions | ### 3.2 Boolean | xUnit | MSTest | |---|---| | `Assert.True(x)` | `Assert.IsTrue(x)` | | `Assert.False(x)` | `Assert.IsFalse(x)` | | `Assert.True(x, "msg")` | `Assert.IsTrue(x, "msg")` | ### 3.3 Type checks | xUnit | MSTest | |---|---| | `Assert.IsType<T>(x)` (exact type, returns `T`) | `var t = Assert.IsExactInstanceOfType<T>(x);` (MSTest 4.1+; returns the typed value, exact match) | | `Assert.IsNotType<T>(x)` (exact type) | `Assert.IsNotExactInstanceOfType<T>(x);` (MSTest 4.1+) | | `Assert.IsAssignableFrom<T>(x)` | `Assert.IsInstanceOfType<T>(x)` -- semantically equivalent (assignable-from check) | > MSTest 4.1+ adds `Assert.IsExactInstanceOfType<T>(x)` -- the proper equivalent of xUnit's exact-type `Assert.IsType<T>` (returns `T`, single call). On pre-4.1 MSTest, fall back to `var t = Assert.IsInstanceOfType<T>(x); Assert.AreEqual(typeof(T), x.GetType());`. `Assert.IsInstanceOfType<T>(x)` on its own is **assignable-only** (= xUnit `Assert.IsAssignableFrom<T>`); silently mapping `IsType<T>` to it loses exact-type semantics. > > MSTest v4's `Assert.IsInstanceOfType<T>(x)` returns the typed value (no out param). MSTest v3 uses `Assert.IsInstanceOfType<T>(x, out var typed)`. ### 3.4 Numeric / comparison | xUnit | MSTest | |---|---| | `Assert.InRange(value, low, high)` | `Assert.IsInRange(value, low, high)` | | `Assert.NotInRange(value, low, high)` | `Assert.IsNotInRange(value, low, high)` | | *(no direct API)* | `Assert.IsGreaterThan(low, value)` | | *(no direct API)* | `Assert.IsLessThan(high, value)` | ### 3.5 String | xUnit | MSTest | |---|---| | `Assert.Contains("sub", str)` | `Assert.Contains("sub", str)` (MSTest 3.8+); fallback `StringAssert.Contains(str, "sub")` | | `Assert.DoesNotContain("sub", str)` | `Assert.DoesNotContain("sub", str)` (MSTest 3.8+); fallback `StringAssert.DoesNotMatch(...)` | | `Assert.StartsWith("p", str)` | `Assert.StartsWith("p", str)` (MSTest 3.8+); fallback `StringAssert.StartsWith(str, "p")` | | `Assert.EndsWith("s", str)` | `Assert.EndsWith("s", str)` (MSTest 3.8+); fallback `StringAssert.EndsWith(str, "s")` | | `Assert.Matches("\\d+", str)` | `Assert.MatchesRegex(@"\d+", str)` | | `Assert.DoesNotMatch("\\d+", str)` | `Assert.DoesNotMatchRegex(@"\d+", str)` | | `Assert.Equal("a", "A", ignoreCase: true)` | `Assert.AreEqual("a", "A", ignoreCase: true)` | ### 3.6 Collection | xUnit | MSTest | |---|---| | `Assert.Contains(item, collection)` | `Assert.Contains(item, collection)` | | `Assert.DoesNotContain(item, collection)` | `Assert.DoesNotContain(item, collection)` | | `Assert.Contains(collection, x => predicate)` | `Assert.IsTrue(collection.Any(x => predicate))` | | `Assert.Empty(collection)` | `Assert.IsEmpty(collection)` | | `Assert.NotEmpty(collection)` | `Assert.IsNotEmpty(collection)` | | `Assert.Single(collection)` | `var item = Assert.ContainsSingle(collection);` (returns the element) | | `Assert.Single(collection, predicate)` | `var item = Assert.ContainsSingle(collection.Where(predicate));` | | `Assert.Collection(items, e1 => ..., e2 => ...)` | **Manual** -- assert count, then per-element. No idiomatic MSTest equivalent | | `Assert.All(items, x => assertion(x))` | **Manual** -- `foreach (var x in items) assertion(x);` | | `Assert.Equal(expected, actual)` on `IEnumerable<T>` (element-wise) | `Assert.AreSequenceEqual(expected, actual)` (MSTest 4.3+); pre-4.3: `CollectionAssert.AreEqual(expected.ToList(), actual.ToList())` (`IList` required). Plain `Assert.AreEqual` does **not** compare element-wise (MSTEST0065). | | `Assert.Equal(expected, actual, comparer)` on collections | `Assert.AreSequenceEqual(expected, actual, comparer)` (MSTest 4.3+); pre-4.3: `CollectionAssert.AreEqual(expected.ToList(), actual.ToList(), comparer)` | | `Assert.Distinct(collection)` | **Manual** -- `Assert.AreEqual(collection.Count, collection.Distinct().Count())` | | `Assert.Superset(expected, actual)` | **Manual** -- `Assert.IsTrue(expected.IsSubsetOf(actual))` if both are `HashSet<T>` | ### 3.7 Exceptions > **Semantic trap**: xUnit `Assert.Throws<T>` = **exact type**. xUnit `Assert.ThrowsAny<T>` = **derived types also match**. The names invert between the frameworks. | xUnit | MSTest | |---|---| | `Assert.Throws<T>(() => ...)` | **`Assert.ThrowsExactly<T>(() => ...)`** | | `Assert.ThrowsAny<T>(() => ...)` | **`Assert.Throws<T>(() => ...)`** | | `Assert.Throws<T>(paramName, () => ...)` (ArgumentException family) | `var ex = Assert.ThrowsExactly<T>(() => ...); Assert.AreEqual(paramName, ex.ParamName);` | | `Record.Exception(() => ...)` | **Manual** -- `try { ...; return null; } catch (Exception ex) { return ex; }`. If you only need to assert a specific type, use `Assert.ThrowsExactly<T>` directly | ### 3.8 Async exception assertions | xUnit | MSTest | |---|---| | `await Assert.ThrowsAsync<T>(() => task)` | `await Assert.ThrowsExactlyAsync<T>(() => task)` | | `await Assert.ThrowsAnyAsync<T>(() => task)` | `await Assert.ThrowsAsync<T>(() => task)` | | `await Record.ExceptionAsync(() => task)` | **Manual** -- `try { await task; return null; } catch (Exception ex) { return ex; }` | ### 3.9 Skip / inconclusive > xUnit `Assert.Skip*` is **runtime** (decided inside the test body). MSTest `[Ignore]` is **compile-time** (decided at discovery). They are not interchangeable -- mapping `SkipUnless` to `[Ignore]` will permanently exclude the test on machines where it should have run. > > **Prefer MSTest's condition attributes** (`[OSCondition]` and `[CICondition]` -- MSTest 3.10+) over `Assert.Inconclusive` when the condition is OS- or CI-environmental. They are discoverable, reportable per-condition, and do not pollute the test body with skip plumbing. (MSTest does **not** ship an `ArchitectureCondition` or `NonParallelizableCondition` attribute -- for architecture gating fall back to runtime `Assert.Inconclusive`; for "do not run in parallel" use `[DoNotParallelize]`.) | xUnit | MSTest | |---|---| | `[Fact(Skip = "reason")]` | `[TestMethod]` + `[Ignore("reason")]` | | `Assert.Skip("reason")` (xUnit v3) | `Assert.Inconclusive("reason")` | | `Assert.SkipWhen(condition, "reason")` (xUnit v3) | If `condition` is environmental: `[OSCondition(...)]` / `[CICondition(...)]` / etc. Otherwise: `if (condition) Assert.Inconclusive("reason");` | | `Assert.SkipUnless(condition, "reason")` (xUnit v3) | Same -- prefer a condition attribute when the predicate is environmental; otherwise `if (!condition) Assert.Inconclusive("reason");` | | `Assert.SkipUnless(OperatingSystem.IsWindows(), "...")` | `[OSCondition(OperatingSystems.Windows)]` on the method | | `Assert.SkipWhen(Environment.GetEnvironmentVariable("CI") != null, "...")` | `[CICondition(ConditionMode.Exclude)]` on the method | ### 3.10 Fail | xUnit | MSTest | |---|---| | `Assert.Fail("reason")` | `Assert.Fail("reason")` | ### 3.11 No-equivalent assertions These xUnit assertions have no MSTest equivalent. Convert each manually: | xUnit | Manual replacement | |---|---| | `Assert.Collection(items, e1Inspector, e2Inspector, ...)` | `Assert.HasCount(N, items); var arr = items.ToArray(); e1Inspector(arr[0]); ...` | | `Assert.All(items, inspector)` | `foreach (var item in items) inspector(item);` | | `Assert.Equivalent(expected, actual)` | Deep-compare manually, or use FluentAssertions / Verify | | `Assert.Raises<T>(addHandler, removeHandler, () => trigger())` | Manual subscribe/flag/unsubscribe | | `Assert.RaisesAny<T>(...)` | Same -- manual handler | | `Assert.PropertyChanged(notifier, "Prop", () => action)` | Subscribe to `INotifyPropertyChanged.PropertyChanged`, set a flag, assert | | `Assert.PropertyChangedAsync(notifier, "Prop", async () => action)` | Same, with `await` | ## 4. Fixtures and lifecycle ### Test-class lifecycle (per-test) | xUnit | MSTest | |---|---| | Constructor (sync setup) | Keep the constructor (MSTest also instantiates one instance per test method) | | Constructor taking `ITestOutputHelper output` | Constructor taking `TestContext testContext` (MSTest 3.6+) | | `Dispose()` | Keep `Dispose()` (MSTest supports `IDisposable`) **or** convert to `[TestCleanup] public void Cleanup()` | | `IAsyncDisposable.DisposeAsync()` | Keep `DisposeAsync()` (MSTest supports `IAsyncDisposable`) **or** `[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 initialization (it allows `readonly` fields and works correctly with nullability). Use `[TestInitialize]` only for async setup or when `TestContext` is needed but you have not adopted constructor injection. ### Class-level fixtures (shared across tests in one class) xUnit `IClassFixture<T>` -- one fixture instance per test class, shared by every test method in that class: ```csharp // xUnit public class DbFixture : IDisposable { /* ... */ } public class OrderTests : IClassFixture<DbFixture> { private readonly DbFixture _fixture; public OrderTests(DbFixture fixture) => _fixture = fixture; } ``` ```csharp // 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(); } ``` ### Cross-class fixtures (`ICollectionFixture<T>` / `[CollectionDefinition]`) xUnit collections do two things at once: (1) share a fixture instance across multiple test classes, **and** (2) serialize execution of those classes (no parallel execution within a collection). MSTest decouples these: - **Sharing** -> `[AssemblyInitialize]` (genuinely process-wide) **or** static `Lazy<T>` shared helper referenced by each class's `[ClassInitialize]` - **Serialization** -> `[DoNotParallelize]` on each member class Map deliberately: | xUnit collection setup | MSTest equivalent | |---|---| | `[CollectionDefinition("Db")]` + `ICollectionFixture<DbFixture>`, member classes have `[Collection("Db")]`, parallelization default | Static `Lazy<DbFixture>` helper + `[ClassInitialize]` per class. No `[DoNotParallelize]` needed | | Same but `[CollectionDefinition("Db", DisableParallelization = true)]` | Same as above + `[DoNotParallelize]` on each member class | | Genuinely process-wide singleton (e.g., `WebApplicationFactory` for a TestServer the whole assembly hits) | `[AssemblyInitialize]` + `[AssemblyCleanup]` in a dedicated `AssemblySetup` class -- with the user's explicit acknowledgement that scope widens to the whole assembly | | Custom `ITestCollectionOrderer` | **Manual** -- MSTest's `[TestMethodAttribute]` ordering model is different; flag for review | ### Assembly-level fixtures | xUnit | MSTest | |---|---| | *(no built-in -- emulated via assembly-scoped `[CollectionDefinition]` + `ICollectionFixture<T>`)* | `[AssemblyInitialize] public static void AssemblyInit(TestContext context)` and `[AssemblyCleanup] public static void AssemblyCleanup()` -- in any class marked `[TestClass]` | ## 5. Output / TestContext | xUnit | MSTest | |---|---| | `ITestOutputHelper` constructor parameter | `TestContext` constructor parameter (MSTest 3.6+) or `public TestContext TestContext { get; set; } = null!;` property | | `_output.WriteLine("...")` | `_testContext.WriteLine("...")` | | `_output.WriteLine("fmt {0}", arg)` (xUnit v2) | `_testContext.WriteLine($"fmt {arg}")` (interpolation -- MSTest v4 dropped most format-string overloads) | | `TestContext.Current.TestOutputHelper.WriteLine(...)` (xUnit v3) | `_testContext.WriteLine(...)` | | `TestContext.Current.AddAttachment(name, contents)` (xUnit v3) | `_testContext.AddResultFile(pathOnDisk)` | | `TestContext.Current.TestMethod.MethodInfo.Name` (xUnit v3) | `_testContext.TestName` | | `TestContext.Current.TestClass.Class.Name` (xUnit v3) | `_testContext.FullyQualifiedTestClassName` | ## 6. Cancellation and timeouts (xUnit v3 specifics) | xUnit v3 | MSTest | |---|---| | `TestContext.Current.CancellationToken` | `_testContext.CancellationToken` (MSTest 3.6+; instance `TestContext` from constructor or property injection -- **never** replace with a new `CancellationTokenSource`, that breaks linkage to test-host cancellation) | | `[Fact(Timeout = 5000)]` | `[Timeout(5000)]` | | `[Fact(Timeout = -1)]` (no timeout) | Omit `[Timeout]` (MSTest default = no timeout) | xUnit v2 has no equivalent of `TestContext.Current.CancellationToken` -- skip this row for v2 sources. ## 7. Parallelization | xUnit default | MSTest equivalent | |---|---| | Parallel across test classes, serial within a class | `[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]` | | xUnit + `[CollectionBehavior(DisableTestParallelization = true)]` | Omit `[assembly: Parallelize]` | | xUnit + `[CollectionBehavior(MaxParallelThreads = N)]` | `[assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)]` | | `[Collection("Db")]` (forces serial within the collection) | `[DoNotParallelize]` on each member class | | `[CollectionDefinition("Db", DisableParallelization = true)]` | Same -- `[DoNotParallelize]` on each member class | > Do not use `ExecutionScope.MethodLevel` to "match xUnit". MethodLevel parallelizes methods *within* a class, which xUnit never does. ### `xunit.runner.json` Delete `xunit.runner.json` after porting the settings that affect behavior: | xUnit setting | MSTest equivalent | |---|---| | `"parallelizeAssembly": false` | MSTest's serial default; omit `[assembly: Parallelize]` | | `"parallelizeTestCollections": false` | MSTest's serial default; omit `[assembly: Parallelize]` | | `"maxParallelThreads": N` | `[assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)]` | | `"diagnosticMessages": true` | Use diagnostic CLI output or `.runsettings` verbosity | | `"longRunningTestSeconds": N` | `[Timeout(N * 1000)]` on affected tests | | `"methodDisplay"` / `"preEnumerateTheories"` / `"appDomain"` | No direct equivalent; document the behavior change | ## 8. Assembly-level attributes xUnit assembly attributes split into two groups: a few have direct MSTest equivalents (and stay at assembly scope); the rest must be removed or reimplemented against MSTest extensibility. | xUnit | Disposition | |---|---| | `[assembly: CollectionBehavior(...)]` | Remove -- replaced by `[assembly: Parallelize(...)]` (Section 7) | | `[assembly: TestCaseOrderer(...)]` | Remove + reimplement with MSTest extensibility if needed (flag for manual) | | `[assembly: TestCollectionOrderer(...)]` | Remove + flag for manual | | `[assembly: TestFramework(...)]` | Remove | | `[assembly: CaptureConsole]` (xUnit v3) | Remove -- MSTest does not capture console by default | | `[assembly: Xunit.Trait("Category", "v")]` | `[assembly: TestCategory("v")]` (applies the category to every test in the assembly -- `TestCategoryAttribute` targets `Assembly`, `Class`, and `Method`) | | `[assembly: Xunit.Trait("k", "v")]` (non-category key) | **No direct equivalent at assembly scope** -- `TestPropertyAttribute` targets only `Class`/`Method`. Either collapse to `[assembly: TestCategory("v")]` if the value alone filters cleanly, or push down to every test class as `[TestProperty("k", "v")]` | ## 9. Packages **Remove** every xUnit package from `.csproj`, `Directory.Build.props`, `Directory.Packages.props`: - `xunit`, `xunit.abstractions`, `xunit.assert`, `xunit.core` - `xunit.extensibility.core`, `xunit.extensibility.execution` - `xunit.runner.visualstudio` - `xunit.v3`, `xunit.v3.assert`, `xunit.v3.core`, `xunit.v3.extensibility.core` - `xunit.v3.mtp-v1`, `xunit.v3.mtp-v2`, `xunit.v3.core.mtp-v1`, `xunit.v3.core.mtp-v2` - `YTest.MTP.XUnit2` (xUnit v2 MTP shim) **Add** MSTest v4 -- pick exactly one of: The versions below are illustrative known-compatible pins. Resolve the current stable versions from the project's configured package source and pin those exact versions during a real migration. ```xml <!-- Option A: metapackage (includes Microsoft.NET.Test.Sdk + TestAdapter + TestFramework + Analyzers) --> <PackageReference Include="MSTest" Version="4.1.0" /> <!-- Keep VSTest explicit and reviewable when that is the source runner. --> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> ``` The explicit `Microsoft.NET.Test.Sdk` line intentionally duplicates the metapackage's transitive dependency: when preserving VSTest, retain and update a source project's explicit pin so runner and version compatibility remain reviewable. For the illustrated MSTest 4.1.0 pin, use 18.0.1+; keeping 17.x causes `NU1605`. Do not add an explicit pin to an MTP project that did not have one. ```xml <!-- Option B: MSTest.Sdk -- 4.1.0 is an example resolved pin; it defaults to MTP. --> <!-- Set <UseVSTest>true</UseVSTest> to preserve VSTest. UseVSTest supplies Microsoft.NET.Test.Sdk, but retain a source project's existing explicit pin. --> <Project Sdk="MSTest.Sdk/4.1.0"> <PropertyGroup> <!-- Keep the project's existing TargetFramework; do not change it during migration. --> <UseVSTest>true</UseVSTest> <!-- omit this line to stay on MTP --> </PropertyGroup> </Project> ``` Prefer pinning the resolved exact `MSTest.Sdk` version in `global.json` (especially in solutions with several test projects) so the version lives in one place. The `4.1.0` below remains the same illustrative pin: ```json { "msbuild-sdks": { "MSTest.Sdk": "4.1.0" } } ``` With the pin in `global.json`, the project line simplifies to `<Project Sdk="MSTest.Sdk">`. `MSTest.Sdk` adds `Microsoft.VisualStudio.TestTools.UnitTesting` as an **implicit global using**, so: - **Do not** add `<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />` to the project file -- it's redundant noise. - **Do not** add `using Microsoft.VisualStudio.TestTools.UnitTesting;` to each test file -- it's already in scope. (Option A -- the `MSTest` metapackage -- does not bring the global using; per-file `using Microsoft.VisualStudio.TestTools.UnitTesting;` is still required there.) ## 10. Companion / extension libraries | xUnit companion | MSTest equivalent | |---|---| | `Xunit.SkippableFact` (`[SkippableFact]`, `Skip.If`, `Skip.IfNot`) | `[Ignore]` (compile-time) or `Assert.Inconclusive("reason")` (runtime). Remove the package | | `Xunit.Combinatorial` (`[CombinatorialData]`, `[CombinatorialValues]`) | [`Combinatorial.MSTest`](https://github.com/Youssef1313/Combinatorial.MSTest) (community port) -- attribute surface is the same as xUnit.Combinatorial. Alternatively, expand combinations into explicit `[DataRow]`s or compute them in `[DynamicData]` | | `Xunit.StaFact` (`[StaFact]`, `[WpfFact]`) | No equivalent -- manual STA thread or flag for review | | `Xunit.Priority` (`[TestCaseOrderer]`) | MSTest ordering is different -- flag for manual | | `Verify.Xunit` | `Verify.MSTest` (swap the package; same usage) | | `FluentAssertions` / `Shouldly` / `AwesomeAssertions` | Keep -- assertion libraries are framework-agnostic. (`AwesomeAssertions` is a fork of `FluentAssertions` and ships in the `FluentAssertions` namespace for API compat -- no source changes needed.) | | `Moq` / `NSubstitute` / `FakeItEasy` | Keep -- mocking libraries are framework-agnostic | | `AutoFixture.Xunit2` (`[AutoData]`) | `AutoFixture` core works, but the auto-data attribute integration requires the xUnit-specific package -- flag for manual |
-
-
SKILL.md 15.1 KB
--- name: migrate-xunit-to-mstest description: > Convert .NET tests from xUnit.net v2/v3 to MSTest v4 while preserving VSTest or MTP. Use for replacing xunit packages, Fact/Theory/InlineData/MemberData, assertions, IClassFixture/ICollectionFixture, ITestOutputHelper, TestContext cancellation, traits/Owner, skips, timeouts, and xUnit parallelization. Also use when a "convert xUnit to MSTest" request may already be migrated: inspect and report the no-op. Do not use for xUnit v2-to-v3, MSTest upgrades, NUnit/TUnit conversion, or runner-only VSTest-to-MTP migration. license: MIT --- # xUnit -> MSTest Migration Convert xUnit.net v2 or v3 tests to MSTest v4 without changing the target framework or test platform. A successful migration builds, discovers the same tests, and preserves pass/fail results and execution semantics. ## Scope Use this skill only when the project contains xUnit packages or source and the user wants MSTest. If the project already uses MSTest and contains no xUnit tests, report that no framework migration is needed and make no changes. Do not combine this framework conversion with a target-framework upgrade or VSTest/MTP migration. Complete and verify one migration before starting another. ## Workspace Contract - Continue after skill activation. Search the current working directory for the staged project and source; never look for user files under this skill's base directory. - Open the literal paths returned by glob/search. If one reader or patch tool rejects a path that was just found, retry with another available tool. Do not ask the user for a path until current-workspace discovery is exhausted. - Classify by the requested deliverable: "convert this project" means edit, build, and test; "give me a plan" or "how would I convert it?" means answer. Do not replace execution with "please provide the files" when files are present. - The final response must state the source xUnit version, preserved runner, changed files, each high-risk semantic mapping applied, and actual test counts. Assertions about fixture lifetime, Owner mapping, cancellation, or parallelization must be visible in the resulting source, not only prose. ## Response Mode - **Full migration request:** inspect the project, make the edits, build, and run tests. Do not stop after giving a plan. - **Focused compile error or API question:** inspect the relevant code and apply only that mapping. Do not narrate the entire workflow. - **Unsupported target framework:** stop before changing packages. MSTest v4 requires .NET 8+ or .NET Framework 4.6.2+ for test applications; offer a separately approved TFM upgrade or MSTest v3 as the intermediate target. ## Decisions That Change the Result Apply these before the mechanical mapping: | Detected state | Required action | |---|---| | No xUnit package, namespace, attribute, or fixture remains | Stop. Make no file changes, report that migration is unnecessary, and run the existing `dotnet test` command once to prove the already-MSTest project is healthy. | | Source uses VSTest | Keep the existing VSTest property/configuration. Prefer retaining and updating a source project's explicit `Microsoft.NET.Test.Sdk` pin; a repository that intentionally relies on the MSTest metapackage's transitive dependency may keep that convention. Do not introduce MTP properties. | | Source uses MTP | Replace xUnit-specific MTP selection with MSTest MTP configuration. Prefer `MSTest.Sdk`; with the metapackage, set `EnableMSTestRunner=true` and `OutputType=Exe`. Preserve native-versus-bridged command integration, and do not add `<UseVSTest>true</UseVSTest>` or other VSTest-only configuration. | | Source relies on xUnit's default parallelization | Add `[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]` to a compiled `.cs` file when the current project has at least two independently runnable test classes. In a one-class project with no explicit parallel setting, omit it because class-level concurrency is not observable. Translate explicit `CollectionBehavior` or `xunit.runner.json` settings regardless of current class count. Before reporting completion, read the changed file back and name it in the result. | For detailed mappings and examples, search [`references/mapping-cheatsheet.md`](references/mapping-cheatsheet.md) for constructs actually present in the project and read only the matching sections. Do not load or reproduce the whole reference. ## Fast Path For a routine project migration, converge in four phases: one batched discovery read/search, one edit pass, one `dotnet test`, and one concise result. Do not: - list a directory and then reread the same files through another tool - copy project files into the loaded skill or its `references/` directory; those files are read-only guidance, not an editing workspace - try `dotnet test --no-restore` unless restore is already known to be current - run separate restore, build, and test commands when `dotnet test` is sufficient - rerun a passing test command or inspect unchanged files for confirmation Use an existing CI/test result as the parity baseline when available. Run a new pre-edit baseline only when counts are unavailable and the migration contains data-driven tests, fixtures, skips, custom extensions, shared state, or other behavior whose parity cannot be established from source alone. ## Workflow ### 1. Establish the baseline 1. 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. 2. State the detected source version: - `xunit` 2.x and related packages -> xUnit v2 - `xunit.v3` or `xunit.v3.*` -> xUnit v3 3. Identify VSTest or MTP from the project and repository configuration. Use `platform-detection` only when the platform is ambiguous, and preserve the detected platform. 4. Record the target frameworks and stop if MSTest v4 does not support them. 5. If the Fast Path requires a new baseline, run the existing test command once and record discovered, passed, failed, and skipped counts. 6. 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: ```xml <!-- 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 MSTest `Assert.ThrowsExactly<T>`. - xUnit `Assert.ThrowsAny<T>` permits derived types and maps to MSTest `Assert.Throws<T>`. - xUnit `Assert.IsType<T>` is exact-type and maps to the generic `Assert.IsExactInstanceOfType<T>`; `Assert.IsAssignableFrom<T>` maps to the generic `Assert.IsInstanceOfType<T>`. When the xUnit assertion's typed return value is assigned, preserve that assignment and use the generic MSTest overload rather than a non-generic `Type` overload. - xUnit `Assert.Equal` on sequences compares elements. Use `Assert.AreSequenceEqual` on MSTest 4.3+ or `CollectionAssert.AreEqual` with materialized lists on earlier v4; never replace sequence equality with reference-based `Assert.AreEqual`. - `[Ignore]` and `[Timeout]` are modifiers; keep `[TestMethod]` so the test is discovered. - `[DataRow]` values must exactly match parameter types. - `TestContext.Current.CancellationToken` maps to an injected MSTest `TestContext.CancellationToken`; never replace it with `CancellationToken.None` or a new `CancellationTokenSource`. - `Owner` is 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`/`IAsyncDisposable` when valid. Map `IAsyncLifetime` to `[TestInitialize]`/`[TestCleanup]`. - `IClassFixture<T>` means one fixture instance per test class, shared by all methods in that class. Map it to a static `T` field created once by a static `[ClassInitialize]` method that accepts `TestContext`, and dispose it once from static `[ClassCleanup]`. Never use `[TestInitialize]`/`[TestCleanup]` for this mapping because that creates one fixture per test method. - For `ICollectionFixture<T>`, preserve both sharing and serialization. Prefer a static `Lazy<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 `ITestOutputHelper` with constructor-injected or property-based MSTest `TestContext`, and replace each `_output.WriteLine(...)` call with the corresponding `TestContext.WriteLine(...)`. In the final result, name both the `TestContext` injection/property and the `WriteLine` mapping explicitly; "migrated output" is not enough to demonstrate parity. xUnit runs classes in parallel by default; MSTest runs them serially. When the current project has two or more independently runnable test classes, preserve that effective behavior with: ```csharp [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)] ``` For a one-class project with no explicit xUnit parallel setting, do not add an assembly policy: there is no class-level concurrency to preserve. Always translate explicit `CollectionBehavior` or `xunit.runner.json` settings. 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 1. Run tests once with the same platform, filter, and configuration used for the baseline. `dotnet test` builds by default; run a separate build only when needed to isolate a compilation failure. 2. Compare discovered, passed, failed, and skipped counts. 3. 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 4. Confirm no xUnit package, namespace, attribute, runner configuration, or fixture interface remains unless explicitly documented for manual follow-up. 5. After the final passing test, read back each changed file that implements a high-risk mapping, plus any runner or parallelization configuration. In the result, name the file and the exact target APIs that implement its lifecycle, data, skip, output, assertion, fixture-scope, or parallelization behavior. Preserve type arguments and member names such as `[ClassInitialize]`, `TestContext.WriteLine`, and `Assert.IsExactInstanceOfType<T>`; generic claims such as "converted attributes" or "migrated output" are not evidence of parity. Keep the final response concise and outcome-focused: - **Changed:** name the files and exact high-risk mappings applied. - **Verified:** give the final test command and discovered/passed/failed/skipped counts. - **Preserved:** state the unchanged target framework and test platform, plus any fixture or parallelization scope decision. - **Remaining:** identify manual follow-up, or say none. ## Completion Criteria - Current xUnit version and test platform were identified - xUnit packages and source constructs were converted - Target framework and test platform stayed unchanged - Fixture scope and effective parallelization decisions are explicit, including justified omission when concurrency is not observable - Build succeeds - Test discovery and result counts match the baseline - Any unsupported custom extension point is called out rather than approximated ## Follow-up Run `migrate-vstest-to-mtp` separately if the user also wants MTP. Use `writing-mstest-tests` only after parity is established to polish the converted MSTest code.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.