From f87d13fcd56d05a2340254ab227a7912a9a612d9 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:41:12 +0200 Subject: [PATCH 1/3] migrate-xunit-to-mstest: add learnings from dotnet/sdk migration Refines the skill based on a real migration of 5 test projects in dotnet/sdk: - MSTest.Sdk implicit global using: do not add `` or per-file `using` in MSTest.Sdk projects (it's already in scope). Note Option A (`MSTest` metapackage) still needs the per-file using. - `Assert.IsExactInstanceOfType` (MSTest 4.1+) is the proper single-call equivalent of xUnit's exact-type `Assert.IsType`; previous guidance silently degraded it to assignable semantics (closes #755). - `Assert.AreSequenceEqual` (MSTest 4.3+) is the modern element-wise equivalent of xUnit's `Assert.Equal` on `IEnumerable`, avoiding the `CollectionAssert` + `.ToList()` dance and the MSTEST0065 trap on plain `AreEqual`. - Clarify `AwesomeAssertions` ships in the `FluentAssertions` namespace, so it's a no-source-change swap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/migrate-xunit-to-mstest/SKILL.md | 6 ++++-- .../references/mapping-cheatsheet.md | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/dotnet-test/skills/migrate-xunit-to-mstest/SKILL.md b/plugins/dotnet-test/skills/migrate-xunit-to-mstest/SKILL.md index 10ee21a192..c1ada6b965 100644 --- a/plugins/dotnet-test/skills/migrate-xunit-to-mstest/SKILL.md +++ b/plugins/dotnet-test/skills/migrate-xunit-to-mstest/SKILL.md @@ -150,13 +150,15 @@ With the pin in `global.json`, the project line simplifies to `` to the project (it's noise) and skip the per-file `using Microsoft.VisualStudio.TestTools.UnitTesting;` in Step 4 -- you only need it for projects on Option A (the `MSTest` metapackage). + ### Step 3: Update project configuration 1. **Preserve the runner.** Confirm the platform decision from Step 1 still holds after Step 2. Common mistakes: - Switching to `MSTest.Sdk` without `UseVSTest=true` silently flips a VSTest project to MTP. Add `true` to the project (the SDK pulls in `Microsoft.NET.Test.Sdk` automatically -- no manual `PackageReference` needed). - `true` 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). 2. Delete `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. -3. Remove `using Xunit;` and `using Xunit.Abstractions;` from C# files (the rewriter will add `using Microsoft.VisualStudio.TestTools.UnitTesting;` instead in Step 4). +3. Remove `using Xunit;` and `using Xunit.Abstractions;` from C# files. For **Option A** (`MSTest` metapackage), Step 4's rewriter will add `using Microsoft.VisualStudio.TestTools.UnitTesting;` per file. For **Option B** (`MSTest.Sdk`), skip the per-file using -- the SDK provides it as an implicit global using. ### Step 4: Convert test classes and methods @@ -213,7 +215,7 @@ Most common cases inline. For the full table including string/collection/type/nu | `Assert.Throws(() => ...)` | **`Assert.ThrowsExactly(() => ...)`** (see trap below) | | `Assert.ThrowsAny(() => ...)` | **`Assert.Throws(() => ...)`** | | `await Assert.ThrowsAsync(...)` | `await Assert.ThrowsExactlyAsync(...)` | -| `Assert.IsType(x)` / `Assert.IsAssignableFrom(x)` | `Assert.IsInstanceOfType(x)` (MSTest v4 returns the typed value) | +| `Assert.IsType(x)` / `Assert.IsAssignableFrom(x)` | `Assert.IsExactInstanceOfType(x)` (MSTest 4.1+, exact type) / `Assert.IsInstanceOfType(x)` (assignable). Both return 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` | diff --git a/plugins/dotnet-test/skills/migrate-xunit-to-mstest/references/mapping-cheatsheet.md b/plugins/dotnet-test/skills/migrate-xunit-to-mstest/references/mapping-cheatsheet.md index 1985fdb904..9f90fe7406 100644 --- a/plugins/dotnet-test/skills/migrate-xunit-to-mstest/references/mapping-cheatsheet.md +++ b/plugins/dotnet-test/skills/migrate-xunit-to-mstest/references/mapping-cheatsheet.md @@ -110,10 +110,12 @@ Target framework throughout: **MSTest v4** (the few v3-only spellings are explic | xUnit | MSTest | |---|---| -| `Assert.IsType(x)` (exact type, returns `T`) | MSTest v4: `var t = Assert.IsInstanceOfType(x);` (semantically *assignable*, not exact); for exact-type, follow with `Assert.AreEqual(typeof(T), x.GetType())` | -| `Assert.IsNotType(x)` (exact type) | `Assert.IsNotInstanceOfType(x);` plus `Assert.AreNotEqual(typeof(T), x.GetType())` if exact-type matters | -| `Assert.IsAssignableFrom(x)` | `Assert.IsInstanceOfType(x)` -- semantically equivalent | +| `Assert.IsType(x)` (exact type, returns `T`) | `var t = Assert.IsExactInstanceOfType(x);` (MSTest 4.1+; returns the typed value, exact match) | +| `Assert.IsNotType(x)` (exact type) | `Assert.IsNotExactInstanceOfType(x);` (MSTest 4.1+) | +| `Assert.IsAssignableFrom(x)` | `Assert.IsInstanceOfType(x)` -- semantically equivalent (assignable-from check) | +> MSTest 4.1+ adds `Assert.IsExactInstanceOfType(x)` -- the proper equivalent of xUnit's exact-type `Assert.IsType` (returns `T`, single call). On pre-4.1 MSTest, fall back to `var t = Assert.IsInstanceOfType(x); Assert.AreEqual(typeof(T), x.GetType());`. `Assert.IsInstanceOfType(x)` on its own is **assignable-only** (= xUnit `Assert.IsAssignableFrom`); silently mapping `IsType` to it loses exact-type semantics. +> > MSTest v4's `Assert.IsInstanceOfType(x)` returns the typed value (no out param). MSTest v3 uses `Assert.IsInstanceOfType(x, out var typed)`. ### 3.4 Numeric / comparison @@ -150,8 +152,8 @@ Target framework throughout: **MSTest v4** (the few v3-only spellings are explic | `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` (element-wise) | `CollectionAssert.AreEqual(expected.ToList(), actual.ToList())` (`IList` required) | -| `Assert.Equal(expected, actual, comparer)` on collections | `CollectionAssert.AreEqual(expected.ToList(), actual.ToList(), comparer)` | +| `Assert.Equal(expected, actual)` on `IEnumerable` (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` | @@ -365,6 +367,13 @@ Prefer pinning the `MSTest.Sdk` version in `global.json` (especially in solution With the pin in `global.json`, the project line simplifies to ``. +`MSTest.Sdk` adds `Microsoft.VisualStudio.TestTools.UnitTesting` as an **implicit global using**, so: + +- **Do not** add `` 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 | @@ -374,6 +383,6 @@ With the pin in `global.json`, the project line simplifies to ` MSTest" sub-table. The settings have no direct MSBuild-property mapping. -3. Remove `using Xunit;` and `using Xunit.Abstractions;` from C# files. For **Option A** (`MSTest` metapackage), Step 4's rewriter will add `using Microsoft.VisualStudio.TestTools.UnitTesting;` per file. For **Option B** (`MSTest.Sdk`), skip the per-file using -- the SDK provides it as an implicit global using. +3. Remove `using Xunit;` and `using Xunit.Abstractions;` from C# files. For **Option A** (`MSTest` metapackage), add `using Microsoft.VisualStudio.TestTools.UnitTesting;` per file (Step 4 covers this alongside the other rewrites). For **Option B** (`MSTest.Sdk`), skip the per-file using -- the SDK provides it as an implicit global using. ### Step 4: Convert test classes and methods @@ -168,7 +168,7 @@ Apply these rewrites to every C# test file. Class-level first, then method-level - Add `[TestClass]` to every class that contained xUnit `[Fact]`/`[Theory]` methods (xUnit had no class-level requirement). - **Preserve the original class hierarchy.** xUnit projects often use base/derived test classes (shared setup, helper assertions, generic base fixtures); marking classes `sealed` would break that pattern. Sealing is an optional follow-up handled by `writing-mstest-tests`, not part of the mechanical migration. -- Replace `using Xunit;` / `using Xunit.Abstractions;` with `using Microsoft.VisualStudio.TestTools.UnitTesting;`. +- Replace `using Xunit;` / `using Xunit.Abstractions;` with `using Microsoft.VisualStudio.TestTools.UnitTesting;`. **On Option B (`MSTest.Sdk`), skip adding the MSTest using** -- the SDK provides it as an implicit global using, so just remove the `using Xunit;` / `using Xunit.Abstractions;` lines (Step 2 and Step 3 cover this). **Methods:**