Migrate Vstest To Mtp
runceel/ReactiveProperty
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP).
Migrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4.
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install microsoft/testfx migrate-xunit-to-mstest --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .claude/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .claude/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstestType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install microsoft/testfx migrate-xunit-to-mstest --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .agents/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .agents/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install microsoft/testfx migrate-xunit-to-mstest --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .cursor/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .cursor/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/microsoft/testfx.git --path .agents/skills/migrate-xunit-to-mstest--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install microsoft/testfx migrate-xunit-to-mstest --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .gemini/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .gemini/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install microsoft/testfx migrate-xunit-to-mstestInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .github/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .github/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install microsoft/testfx migrate-xunit-to-mstest --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/testfx.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/migrate-xunit-to-mstest .opencode/skills/migrate-xunit-to-mstest && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "migrate-xunit-to-mstest" agent skill from https://github.com/microsoft/testfx/tree/main/.agents/skills/migrate-xunit-to-mstest into .opencode/skills/migrate-xunit-to-mstest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-xunit-to-mstest", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
migrate-xunit-to-mstestMigrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4.
Migrate Xunit To Mstest is an agent skill from microsoft/testfx, published by the product's own GitHub organization. Migrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4. USE FOR: convert/migrate xUnit tests to MSTest, replace xunit/xunit.v3 packages, 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…
Its SKILL.md is about 9.8k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files, including reference files (for example `references/mapping-cheatsheet.md`).
It sits in Testing & QA, covering Unit testing and Code migrations. It works with .NET. The repository describes itself as: This repository holds the source code of Microsoft.Testing.Platform (MTP), a lightweight alternative to VSTest, as well as MSTest adapter and framework. The licence is MIT.
12 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 19d717a. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
dotnetFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
github.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Migrate Xunit To Mstest loads about 9.8k tokens when it runs, and up to ~16k if it reads all its reference files. Until then it costs about 225 tokens; SKILL.md has 4,049 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from microsoft/testfx at commit 19d717a, republished under its MIT licence (© microsoft). 4,049 words, ~9,848 tokens.
.claude/skills/migrate-xunit-to-mstest/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.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 (MSTestmetapackage). Reach for Option B (MSTest.Sdk) only when the user explicitly asks to modernize the SDK or already usesMSTest.Sdkelsewhere 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.Sdkpulls VSTest'sMicrosoft.CodeCoveragetransitively. 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.SdkwithoutUseVSTest) for MTP projects -- the SDK omitsMicrosoft.NET.Test.Sdkand 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]targetsAssembly,Class, andMethod, so an xUnit[assembly: Trait("Category", ...)]keeps its assembly scope under MSTest as[assembly: TestCategory(...)].[TestProperty]targets onlyClassandMethod— there is noAttributeTargets.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 allowsreadonlyfields). Use[TestInitialize]only for async setup or when you needTestContext.
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 staticLazy<<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(noITestOutputHelper). Do NOT replaceTestContext.Current.CancellationTokenwith a newCancellationTokenSource-- 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 defaultUse 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.MethodLevelto "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.© microsoft, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file (references) in .agents/skills/migrate-xunit-to-mstest of microsoft/testfx.
Open the folder on GitHubat commit 19d717a
Migrate Xunit To Mstest next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Migrate Xunit To Mstest this skillmicrosoft/testfx | 1k | — | ~9.8k | Automated safety check: Pass | MIT | |
| Migrate Vstest To Mtprunceel/ReactiveProperty | 944 | — | ~4.3k | Automated safety check: Pass | MIT | |
| Migrate Nunit To Mstestdotnet/skills | 5.6k | 1 repos | ~3.8k | Automated safety check: Pass | MIT | |
| New Event Sourceaws/aws-lambda-dotnet | 1.7k | — | ~3k | Automated safety check: Pass | Apache-2.0 | |
| ScottPlot Test RunnerScottPlot/ScottPlot | 6.8k | — | ~308 | Automated safety check: Pass | MIT | |
| Aspire Integration TestingDevBetterCom/DevBetterWeb | 157 | 2 repos | ~2.3k | Automated safety check: Pass | None |
runceel/ReactiveProperty
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP).
dotnet/skills
Convert .NET tests from NUnit 3/4 to MSTest v4 while preserving VSTest or MTP.
aws/aws-lambda-dotnet
Add a new AWS event source attribute (e.g., Kinesis, Kafka, MQ) to the Lambda .NET Annotations framework, including the attribute class, source generator integration, CloudFormation writer, unit…
ScottPlot/ScottPlot
Run or add ScottPlot 5 tests. Use for unit-test and cookbook-test work; unless explicitly asked otherwise, restrict manual test execution to the Unit Tests…
DevBetterCom/DevBetterWeb
Write integration tests using .NET Aspire's testing facilities with xUnit.
microsoft/vstest
Build, test, and validate changes in the vstest repository. An agent skill from microsoft/vstest.
microsoft/testfx
Guide for organizing MSBuild infrastructure with Directory.Build.props, Directory.Build.targets, Directory.Packages.props, and Directory.Build.rsp.
microsoft/testfx
Project-wide code coverage and CRAP (Change Risk Anti-Patterns) score analysis for .NET projects.
microsoft/testfx
Guide for optimizing MSBuild incremental builds. An agent skill from microsoft/testfx.
microsoft/testfx
Validate TestFx shipping paths and capable CI execution using packed consumers, package/cache provenance, exact exits and artifacts, and selected-versus-executed test evidence.
microsoft/testfx
Guide for modernizing and migrating MSBuild project files to SDK-style format.
microsoft/testfx
Guide for interpreting ResolveProjectReferences time in MSBuild performance summaries.
Works with
Categories
Migrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4. Migrate Xunit To Mstest is an agent skill from microsoft/testfx, published by the product's own GitHub organization.net (v2 or v3) to MSTest v4.
Migrate Xunit To Mstest fits situations like: : convert/migrate xUnit tests to MSTest; replace xunit/xunit.v3 packages; port [Fact]/[Theory]/[InlineData]/[MemberData]/[ClassData] to [TestMethod]/[DataRow]/[DynamicData]; port Assert.Equal/True/Throws/ThrowsAsync to Assert.AreEqual/IsTrue/ThrowsExactly/ThrowsExactlyAsync.
Run `npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a claude-code`. Or copy the skill folder (.agents/skills/migrate-xunit-to-mstest in microsoft/testfx) into .claude/skills/migrate-xunit-to-mstest in your project. Claude Code loads it when a task matches its description.
Run `npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a codex`. Or copy the skill folder (.agents/skills/migrate-xunit-to-mstest in microsoft/testfx) into .agents/skills/migrate-xunit-to-mstest in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add microsoft/testfx --skill migrate-xunit-to-mstest -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/migrate-xunit-to-mstest, .gemini/skills/migrate-xunit-to-mstest, .github/skills/migrate-xunit-to-mstest and .opencode/skills/migrate-xunit-to-mstest in your project.
Going by SKILL.md and its folder, Migrate Xunit To Mstest needs the command-line tools its instructions call (dotnet).
SKILL.md names 1 domain. As links in the text: github.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Migrate Xunit To Mstest is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.8k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 6k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Migrate Xunit To Mstest: Migrate Vstest To Mtp (runceel/ReactiveProperty, 944 stars), Migrate Nunit To Mstest (dotnet/skills, 5.6k stars), New Event Source (aws/aws-lambda-dotnet, 1.7k stars) and ScottPlot Test Runner (ScottPlot/ScottPlot, 6.8k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
microsoft (a GitHub organization, an official publisher) maintains it in microsoft/testfx, which has 1,047 GitHub stars. The repository holds 50 skills in this directory. The repository was last updated on October 9, 2026.
Source: microsoft/testfx on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.