Merge origin/main into bot/weekly-version-sync

Preserve current main content and advance dotnet-test-migration to the next valid release checkpoint.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d007947-f964-4809-a31c-2d9f457a2ba3
This commit is contained in:
Abhitej John
2026-09-09 10:37:10 -07:00
54 changed files with 2128 additions and 153 deletions
@@ -1,7 +1,7 @@
{
"name": "dotnet-test-migration",
"version": "0.1.9",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"version": "0.1.10",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit/NUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"skills": ["./skills/"],
"agents": [
"./agents/test-migration.agent.md"
@@ -1,7 +1,7 @@
{
"name": "dotnet-test-migration",
"version": "0.1.9",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"version": "0.1.10",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit/NUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"skills": ["./skills/"],
"agents": [
"./agents/test-migration.agent.md"
+3 -1
View File
@@ -1,12 +1,13 @@
# dotnet-test-migration
Skills and an orchestrator agent for migrating .NET test frameworks and platforms. Covers framework version upgrades (MSTest, xUnit), cross-framework conversion (xUnit → MSTest), and test platform migration (VSTest → Microsoft.Testing.Platform).
Skills and an orchestrator agent for migrating .NET test frameworks and platforms. Covers framework version upgrades (MSTest, xUnit), cross-framework conversion (xUnit/NUnit → MSTest), and test platform migration (VSTest → Microsoft.Testing.Platform).
## When to use this plugin
- **Upgrade MSTest** — MSTest v1/v2 → v3, then v3 → v4 (handles source and behavioral breaking changes)
- **Upgrade xUnit** — xUnit.net v2 → v3
- **Convert xUnit to MSTest** — port xUnit (v2 or v3) projects to MSTest v4
- **Convert NUnit to MSTest** — port NUnit (v3 or v4) projects to MSTest v4
- **Adopt Microsoft.Testing.Platform** — migrate from the VSTest runner to MTP
- **Orchestrate migrations** — auto-detect the current framework/version/platform and route to the right migration
@@ -18,6 +19,7 @@ Skills and an orchestrator agent for migrating .NET test frameworks and platform
| **migrate-mstest-v3-to-v4** | Upgrade MSTest v3 to v4 — handles all source and behavioral breaking changes |
| **migrate-xunit-to-xunit-v3** | Upgrade xUnit.net v2 to v3 |
| **migrate-xunit-to-mstest** | Convert xUnit.net (v2 or v3) test projects to MSTest v4 — attributes, assertions, fixtures, lifecycle, output, parallelization |
| **migrate-nunit-to-mstest** | Convert NUnit (v3 or v4) test projects to MSTest v4 — constraints, data sources, fixture lifecycle, namespace setup, output, parallelization |
| **migrate-vstest-to-mtp** | Migrate from the VSTest runner to Microsoft.Testing.Platform |
## Agents
@@ -51,6 +51,7 @@ Classify the user's request and route to the appropriate skill or agent:
| "Upgrade MSTest" (v1/v2 detected, user wants v4) | `migrate-mstest-v1v2-to-v3` first, then `migrate-mstest-v3-to-v4` |
| "Migrate to xUnit v3" / "upgrade xUnit" | `migrate-xunit-to-xunit-v3` skill |
| "Convert xUnit to MSTest" / "switch from xUnit to MSTest" / "port xUnit tests to MSTest" (xUnit v2 or v3 detected) | `migrate-xunit-to-mstest` skill |
| "Convert NUnit to MSTest" / "switch from NUnit to MSTest" / "port NUnit tests to MSTest" (NUnit 3 or 4 detected) | `migrate-nunit-to-mstest` skill |
| "Migrate to MTP" / "switch from VSTest" / "modern test runner" | `migrate-vstest-to-mtp` skill |
| "Make code testable" / "remove static dependencies" | Hand off to `testability-migration` agent |
| "Migrate my tests" (no specifics) | Run detection, then recommend and confirm the migration path |
@@ -110,6 +111,8 @@ Some migrations must happen in sequence:
| xUnit v2 | xUnit v3 | `migrate-xunit-to-xunit-v3` (single step; v3 has native MTP support) |
| xUnit v2 or v3 | MSTest v4 | `migrate-xunit-to-mstest` (single step; preserves current test platform — VSTest stays VSTest, MTP stays MTP) |
| xUnit v2 or v3 | MSTest v4 + MTP | `migrate-xunit-to-mstest``migrate-vstest-to-mtp` (only if the project was on VSTest before; commit between) |
| NUnit 3 or 4 | MSTest v4 | `migrate-nunit-to-mstest` (single step; preserves current test platform — VSTest stays VSTest, MTP stays MTP) |
| NUnit 3 or 4 | MSTest v4 + MTP | `migrate-nunit-to-mstest``migrate-vstest-to-mtp` (only if the project was on VSTest before; commit between) |
| Any framework | MTP only | `migrate-vstest-to-mtp` (single step) |
**Always commit between migration steps.** Each step should leave the project in a buildable, test-passing state.
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "dotnet-test-migration",
"version": "0.1.9",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"version": "0.1.10",
"description": "Skills and an orchestrator agent for migrating .NET test frameworks and platforms: MSTest and xUnit version upgrades, xUnit/NUnit-to-MSTest conversion, and VSTest to Microsoft.Testing.Platform.",
"skills": ["./skills/"],
"agents": [
"./agents/test-migration.agent.md"
@@ -0,0 +1,193 @@
---
name: migrate-nunit-to-mstest
description: >
Convert .NET tests from NUnit 3/4 to MSTest v4 while preserving VSTest or
MTP. Use for replacing NUnit/NUnit3TestAdapter packages, Test/TestCase/
TestCaseSource/Values attributes, constraint assertions, SetUp/TearDown,
OneTimeSetUp, SetUpFixture, FixtureLifeCycle, TestContext, categories,
retries, timeouts, and NUnit parallelization. Also use when a "convert NUnit
to MSTest" request may already be migrated: inspect and report the no-op. Do
not use for NUnit version upgrades, xUnit/TUnit conversion, MSTest upgrades,
or runner-only VSTest-to-MTP migration.
license: MIT
---
# NUnit -> MSTest Migration
Convert NUnit 3 or 4 tests to MSTest v4 without changing the target framework or test platform. A successful migration builds, discovers the same test cases, and preserves pass/fail, lifecycle, data, filtering, and concurrency semantics.
## Scope
Use this skill only when the project contains NUnit packages or source and the user wants MSTest. If the project already uses MSTest and contains no NUnit 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
- Search the current working directory for projects and source; never look for user files under this skill's directory.
- Classify the deliverable: "convert this project" means edit, build, and test; "give me a plan" means answer without editing.
- Preserve the user's requested project scope. Shared props may be changed only when they own NUnit package or runner configuration for that scope.
- The final response must state the source NUnit version, preserved runner, changed files, high-risk semantic mappings, and actual test counts.
## Response Mode
- **Full migration request:** inspect, edit, build, and run tests. Do not stop after a plan.
- **Focused compile error or API question:** apply only the relevant mapping.
- **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.
## Decisions That Change the Result
| Detected state | Required action |
|---|---|
| No NUnit package, namespace, attribute, or constraint remains | Stop, make no changes, and run the existing test command once to prove the already-MSTest project is healthy. |
| Source uses VSTest | Preserve VSTest. Retain and update an explicit `Microsoft.NET.Test.Sdk` pin when the repository owns one; do not introduce MTP properties. |
| Source uses MTP | Replace NUnit-specific MTP configuration with MSTest MTP configuration. Prefer `MSTest.Sdk`; with the metapackage set `EnableMSTestRunner=true` and `OutputType=Exe`. |
| NUnit uses its default fixture lifecycle | NUnit normally shares one fixture instance across all test cases; MSTest creates a new class instance per test. Move state that must remain class-shared behind static fields initialized by `[ClassInitialize]`, or prove the state is per-test before leaving it as an instance field. |
| NUnit has no `Parallelizable` configuration | Preserve serial execution. Do not add `[assembly: Parallelize]`; NUnit and MSTest are both serial by default. |
| NUnit explicitly enables parallel execution | Translate the effective scope and worker count. Never infer method-level parallelism from fixture-level settings. |
For detailed mappings, search [`references/mapping-cheatsheet.md`](references/mapping-cheatsheet.md) for constructs present in the project and read only those sections.
## Fast Path
For a routine migration, converge in four phases: one batched discovery pass, one edit pass, one `dotnet test`, and one concise result.
- Use an existing CI/test result as the baseline when available.
- Run a pre-edit baseline when counts are unavailable and the project contains parameterized tests, fixture state, namespace setup, retries, custom attributes, or explicit parallelization.
- Do not run separate restore, build, and test commands when `dotnet test` is sufficient.
- Do not delete NUnit runner configuration until its behavior has been translated.
## Workflow
### 1. Establish the baseline
1. Batch-read test projects, central package files, `global.json`, `.runsettings`, `testconfig.json`, and NUnit configuration.
2. Detect NUnit from `NUnit`, `NUnit3TestAdapter`, `NUnit.Analyzers`, `NUnit.Framework`, `NUnit.Framework.Legacy`, and `using NUnit.Framework`.
3. State whether the source is NUnit 3 or 4 from the resolved package version.
4. Detect VSTest or MTP and preserve it. Use `platform-detection` only when ambiguous.
5. Record target frameworks and stop if MSTest v4 does not support them.
6. Inventory high-risk constructs:
- `[TestFixture(...)]`, `[TestFixtureSource]`, `[FixtureLifeCycle]`, constructors with parameters
- `[TestCase]`, `[TestCaseSource]`, `TestCaseData`, `[Values]`, `[Range]`, `[Random]`, `[Sequential]`, `[Pairwise]`, custom data attributes
- `[Theory]`, `[Datapoint]`, `[DatapointSource]`, automatic bool/enum datapoints, `Assume.That`
- `[OneTimeSetUp]`, `[OneTimeTearDown]`, `[SetUpFixture]`, inheritance-based setup
- `Assert.That`, `Assert.Multiple`, `Assert.Throws`, `Assert.Catch`, collection constraints
- `[Parallelizable]`, `[NonParallelizable]`, `[LevelOfParallelism]`, `[Order]`, `[SingleThreaded]`
- `[Apartment]`, `[RequiresThread]`, `[CancelAfter]`, `[Timeout]`
- `[Culture]`, `[Platform]`, `[SetCulture]`, `[SetUICulture]`
- `[Explicit]`, `[Repeat]`, `[Retry]`, `[MaxTime]`, categories, properties, `[TestOf]`
- `[DefaultFloatingPointTolerance]`, `[NonTestAssembly]`, and deprecated fixture lifecycle aliases
### 2. Replace packages without switching runners
Remove NUnit-specific packages being replaced, including `NUnit`, `NUnit3TestAdapter`, `NUnit.Analyzers`, NUnit console runner packages, and NUnit-specific MTP adapters.
Default to the current stable MSTest v4 metapackage resolved from the configured package source:
```xml
<PackageReference Include="MSTest" Version="4.4.0" />
```
The pin is illustrative for the current release; during a real migration resolve and pin the current stable version. Preserve an explicit `Microsoft.NET.Test.Sdk` dependency for VSTest when the source project owns one. When preserving MTP, prefer `MSTest.Sdk`; otherwise use `EnableMSTestRunner=true` and `OutputType=Exe`.
Do not change `TargetFramework`. Remove NUnit `.runsettings` adapter settings only after translating relevant behavior.
### 3. Perform the mechanical conversion
| NUnit | MSTest |
|---|---|
| `[TestFixture]` or fixture with tests | `[TestClass]` |
| `[Test]` | `[TestMethod]` |
| `[TestCase(...)]` | `[TestMethod]` + `[DataRow(...)]` |
| `[TestCaseSource(nameof(Cases))]` | `[TestMethod]` + `[DynamicData(nameof(Cases))]` |
| `[SetUp]` / `[TearDown]` | `[TestInitialize]` / `[TestCleanup]` |
| `[OneTimeSetUp]` / `[OneTimeTearDown]` | static `[ClassInitialize]` / `[ClassCleanup]` |
| `[Category(value)]` | `[TestCategory(value)]` |
| `[Property(key, value)]` | `[TestProperty(key, value)]` |
| `[Ignore("reason")]` | `[Ignore("reason")]` plus `[TestMethod]` |
| `[Timeout(ms)]` | `[Timeout(ms)]` plus `[TestMethod]` |
| `[Retry(n)]` | `[Retry(n - 1)]` plus `[TestMethod]` when `n > 1`; remove it when `n = 1` |
Remove `using NUnit.Framework;` and `using NUnit.Framework.Legacy;`. Add `using Microsoft.VisualStudio.TestTools.UnitTesting;` when using the metapackage; `MSTest.Sdk` supplies an implicit global using.
Do not mechanically seal classes or flatten inherited setup methods.
### 4. Resolve semantic mappings
Load the mapping cheatsheet for every high-risk construct found in Step 1. These rules are mandatory:
- NUnit's default `LifeCycle.SingleInstance` differs from MSTest's per-test class instances. Preserve class-shared fields explicitly.
- `[OneTimeSetUp]` and `[OneTimeTearDown]` become static MSTest methods. Move any instance state they use to a static holder rather than merely adding `static`.
- NUnit `Assert.Throws<T>` and `Throws.TypeOf<T>` require an exact exception type and map to `Assert.ThrowsExactly<T>`. NUnit `Assert.Catch<T>` and `Throws.InstanceOf<T>` permit derived types and map to `Assert.Throws<T>`.
- NUnit `Is.TypeOf<T>` maps to `Assert.IsExactInstanceOfType<T>`; `Is.InstanceOf<T>` maps to `Assert.IsInstanceOfType<T>`.
- NUnit equality constraints compare sequences element-by-element. Use `Assert.AreSequenceEqual` on MSTest 4.3+ or `CollectionAssert.AreEqual` with materialized lists; never use reference-based `Assert.AreEqual` for sequences.
- `TestCase(ExpectedResult=...)` and `TestCaseData.Returns(...)` require rewriting the target test to assert the expected result.
- Preserve per-row names, categories, and ignores by returning `TestDataRow<T>` from `DynamicData` when needed. NUnit row properties have no direct `TestDataRow<T>` equivalent and require a custom data source or an explicit metadata decision.
- MSTest 4.4 combinatorial attributes can map NUnit `[Combinatorial]`, `[Values]`, `[Range]`, and `[Random]`; add `using Microsoft.VisualStudio.TestTools.UnitTesting.Combinatorial;`. Constructor semantics differ, so translate NUnit ranges and random bounds explicitly. `[Sequential]` still requires explicit rows or a custom data source.
- NUnit `[Pairwise]` has no MSTest built-in. Preserve the exact generated rows in `DynamicData` or use a verified pairwise `ITestDataSource`; full Cartesian combinations change test counts and execution cost.
- Convert `[Theory]`, `[Datapoint]`, and `[DatapointSource]` into explicit `DynamicData` or combinatorial sources. Preserve automatic bool/enum values and the rule that a theory fails when every row violates its assumptions.
- `[Explicit]`, `[Repeat]`, parameterized fixtures, fixture sources, and `Assert.Multiple` have no behavior-identical mechanical mapping. Rewrite deliberately or report manual follow-up; never approximate silently.
- NUnit `[Retry(n)]` counts the initial attempt, while MSTest `[Retry(n)]` counts retries after the initial attempt. Subtract one and review NUnit `RetryExceptions` filters separately.
- `[CancelAfter(ms)]` maps to `[Timeout(ms, CooperativeCancellation = true)]`. Under VSTest, remove NUnit's injected `CancellationToken` parameter and use an injected or property-based `TestContext.CancellationToken`; VSTest otherwise treats it as missing data. Retain a method token only when the preserved runner is proven to support injection. Expand fixture-level defaults to every affected test and lifecycle method.
- `[Apartment(ApartmentState.STA)]` maps to `[STATestClass]` or `[STATestMethod]`. Set `UseSTASynchronizationContext = true` when async continuations must remain on the STA thread. MTA is the MSTest default. `[RequiresThread]` and `[SingleThreaded]` guarantee thread identity that MSTest attributes do not generally preserve; use a custom executor or report manual follow-up.
- Map NUnit platform and culture gates to `OSCondition`, `ArchitectureCondition`, or a `MemberCondition` helper. Map `SetCulture` and `SetUICulture` to setup/cleanup that saves and restores the original culture.
- Expand `[DefaultFloatingPointTolerance]` into explicit deltas on every affected assertion. Preserve method-over-fixture-over-assembly precedence.
### 5. Preserve lifecycle and namespace setup
- `[SetUp]` and `[TearDown]` remain per-test lifecycle methods.
- For NUnit's default single fixture instance, audit every mutable instance field. If tests depend on sharing, use static state created by `[ClassInitialize]` and released by `[ClassCleanup]`.
- `[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]` already matches MSTest class instantiation; remove the attribute and keep per-test instance state.
- `[SetUpFixture]` is namespace-scoped in NUnit. MSTest assembly initialization is assembly-wide. Use `[AssemblyInitialize]` only when the NUnit setup already covers the whole assembly; otherwise move setup into the affected classes or a shared helper without widening scope.
- Preserve setup/cleanup inheritance order. Do not merge base and derived methods unless the resulting order is proven equivalent.
- Map NUnit `TestContext.WriteLine` to an injected or property-based MSTest `TestContext.WriteLine`. Translate directory, test-name, and attachment APIs individually.
### 6. Preserve parallelization and ordering
NUnit and MSTest both run serially by default. Do not add parallelization for an unconfigured NUnit project.
When NUnit explicitly opts in:
- assembly `[Parallelizable(ParallelScope.Fixtures)]` -> `[assembly: Parallelize(Workers = N, Scope = ExecutionScope.ClassLevel)]`
- fixture `[Parallelizable(ParallelScope.Children)]` or method-level parallelism -> method-level MSTest parallelization only after confirming shared instance state is safe
- `[NonParallelizable]` -> `[DoNotParallelize]` when parallelization is enabled
- `[LevelOfParallelism(N)]` -> `Workers = N`
- resource-specific serialization can use MSTest 4.4 `[ResourceLock("key")]` when it preserves a narrower lock than `[DoNotParallelize]`
NUnit `[Order]` is not general dependency semantics. Use MSTest 4.4 `[DependsOn]` only when the source truly expresses a prerequisite; otherwise remove ordering by making tests independent or report manual follow-up.
### 7. Verify parity
1. Run tests with the same platform, filter, and configuration used for the baseline.
2. Compare discovered, passed, failed, and skipped counts.
3. Investigate every difference:
- missing rows -> `DataRow`, `DynamicData`, combinatorial, or row metadata conversion
- changed exceptions/types -> exact-vs-derived mapping
- state failures -> NUnit single-instance fixture semantics or setup order
- concurrency failures -> `Parallelize`, `DoNotParallelize`, worker count, or resource locks
- changed skips -> `Ignore`, `Explicit`, row-level ignore, or retry behavior
4. Confirm no NUnit package, namespace, attribute, constraint, adapter setting, or custom NUnit extension remains unless documented for follow-up.
5. Read back high-risk changed files and name the exact target APIs in the result.
Use this final response shape:
- **Changed:** files and exact high-risk mappings.
- **Verified:** final command and discovered/passed/failed/skipped counts.
- **Preserved:** target framework, test platform, fixture scope, and concurrency choice.
- **Remaining:** manual follow-up, or none.
## Completion Criteria
- NUnit version and test platform were identified
- NUnit packages and source constructs were converted
- Target framework and test platform stayed unchanged
- Default single-instance fixture semantics were audited and preserved where observable
- Data-row metadata and assertion semantics were preserved
- Namespace setup and parallelization scope were not silently widened
- Every attribute on NUnit's official attribute index was classified as direct, rewritten, removed, or manual
- Build succeeds and test results match the baseline
- Unsupported custom extensions or behavior are called out
## Follow-up
Run `migrate-vstest-to-mtp` separately if the user also wants MTP. Use `writing-mstest-tests` only after parity is established.
@@ -0,0 +1,251 @@
# NUnit -> MSTest Mapping Cheatsheet
Load only the sections needed for constructs found in the project. Target MSTest is v4; features introduced in a specific v4 release are marked.
## Official attribute coverage index
Every attribute on NUnit's official attribute page is classified below or in a named section of this reference.
| Disposition | NUnit attributes |
|---|---|
| Direct or close mapping | `Apartment`, `Author`, `CancelAfter`, `Category`, `Combinatorial`, `Description`, `FixtureLifeCycle`, `Ignore`, `LevelOfParallelism`, `MaxTime`, `NonParallelizable`, `OneTimeSetUp`, `OneTimeTearDown`, `Parallelizable`, `Property`, `Random`, `Range`, `Retry`, `SetUp`, `SetUpFixture`, `TearDown`, `Test`, `TestCase`, `TestCaseSource`, `TestFixture`, `Timeout`, `Values`, `ValueSource` |
| Explicit semantic rewrite | `Culture`, `Datapoint`, `DatapointSource`, `DefaultFloatingPointTolerance`, `Order`, `Pairwise`, `Platform`, `Repeat`, `Sequential`, `SetCulture`, `SetUICulture`, `TestFixtureSource`, `TestOf`, `Theory` |
| Remove or normalize | `NonTestAssembly`, `TestFixtureSetUp`, `TestFixtureTearDown` |
| No behavior-identical MSTest attribute | `Explicit`, `RequiresThread`, `SingleThreaded` |
## 1. Discovery and metadata
| NUnit | MSTest |
|---|---|
| `[TestFixture]` | `[TestClass]` |
| `[Test]` | `[TestMethod]` |
| `[Test(Description = "x")]` | `[TestMethod(DisplayName = "x")]` when the description is used as the display name |
| `[Category("Unit")]` | `[TestCategory("Unit")]` |
| `[Property("Key", "Value")]` | `[TestProperty("Key", "Value")]` |
| `[Author("name")]` | `[Owner("name")]` only when author is used as ownership metadata; otherwise `[TestProperty("Author", "name")]` |
| `[Description("text")]` | `[Description("text")]` |
| `[Ignore("reason")]` | `[TestMethod]` + `[Ignore("reason")]` |
| `[Explicit("reason")]` | No exact equivalent. MSTest `Ignore` never runs, while NUnit explicit tests run when selected. Preserve through an agreed filtering convention or report manual follow-up. |
Parameterized `[TestFixture(...)]` and `[TestFixtureSource]` have no class-level data equivalent. Split them into concrete `[TestClass]` types, move parameters into data-driven methods, or use a custom generator. Do not discard fixture arguments.
`[TestOf(typeof(T))]` is NUnit property metadata. Map it at class or method scope to `[TestProperty("TestOf", "Namespace.T")]`, preserving the full type name as a string. MSTest `TestProperty` has no assembly target, so push assembly-level metadata to each test class or document its removal.
`[NonTestAssembly]` has no role after NUnit is removed. If the project intentionally contains no tests, remove NUnit and its adapter but do not add MSTest or turn the utility assembly into a test project.
Deprecated `[TestFixtureSetUp]` and `[TestFixtureTearDown]` first normalize to `[OneTimeSetUp]` and `[OneTimeTearDown]`, then map to static `[ClassInitialize]` and `[ClassCleanup]`.
## 2. Data-driven tests
| NUnit | MSTest |
|---|---|
| `[TestCase(1, 2)]` | `[TestMethod]` + `[DataRow(1, 2)]` |
| `[TestCaseSource(nameof(Cases))]` | `[TestMethod]` + `[DynamicData(nameof(Cases))]` |
| `TestCaseData` with name/category/ignore | `TestDataRow<T>` carrying `DisplayName`, `TestCategories`, and `IgnoreMessage` |
| `TestCaseData` custom properties | No direct `TestDataRow<T>` equivalent; use a custom data source or move stable metadata to the test method/class |
| `[Combinatorial]` | `[CombinatorialData]` (MSTest 4.4+) |
| parameter `[Values(...)]` | `[CombinatorialValues(...)]` (MSTest 4.4+) |
| parameter `[Range(from, to)]` | `[CombinatorialRange(from, to, 1)]` (MSTest 4.4+); the two-argument MSTest constructor means `from, count`, not `from, to` |
| parameter `[Range(from, to, step)]` | `[CombinatorialRange(from, to, step)]` (MSTest 4.4+) |
| parameter `[Random(min, max, count)]` | `[CombinatorialRandomData(Minimum = min, Maximum = max, Count = count)]` (MSTest 4.4+) |
| `[Sequential]` | Explicit `[DataRow]` pairs or one `[DynamicData]` source; Cartesian combinatorial data is not equivalent |
| `[ValueSource]` | `[DynamicData]` or a custom `ITestDataSource` |
| `[Pairwise]` | Preserve the exact generated rows in `[DynamicData]`, or use a verified pairwise `ITestDataSource`; `[CombinatorialData]` is not equivalent |
`[TestCase(ExpectedResult = value)]` and `new TestCaseData(...).Returns(value)` require a source rewrite:
```csharp
// NUnit
[TestCase(2, 3, ExpectedResult = 5)]
public int Add(int left, int right) => left + right;
// MSTest
[TestMethod]
[DataRow(2, 3, 5)]
public void Add(int left, int right, int expected)
=> Assert.AreEqual(expected, left + right);
```
MSTest `[DataRow]` values must exactly match parameter types. Audit numeric suffixes, enums, arrays, and nullable values.
MSTest combinatorial attributes live in `Microsoft.VisualStudio.TestTools.UnitTesting.Combinatorial`; add that using when any of them are emitted.
### NUnit theories
NUnit `[Theory]` discovers values by type from `[Datapoint]` and `[DatapointSource]`, combines values across parameters, and supplies all bool or enum values automatically when no explicit datapoints exist. MSTest has no theory attribute with those discovery rules.
1. Gather matching datapoints from the fixture type, including inherited fixture members. When `searchInDeclaringTypes` is enabled, additionally traverse the fixture's lexically enclosing `DeclaringType` chain.
2. Materialize the same rows through `[DynamicData]` or MSTest combinatorial attributes.
3. Explicitly enumerate `false/true` and all enum values that NUnit supplied automatically.
4. Convert `Assume.That(...)` to row-level `Assert.Inconclusive(...)` or an equivalent condition.
5. Capture the source runner's observable all-assumptions-invalid result before editing. NUnit defines an aggregate theory failure, but the NUnit VSTest adapter can expose only skipped child cases and return success. Preserve the actual runner-visible outcome unless the user explicitly wants the framework-level aggregate rule.
## 3. Assertions
### Equality and collections
| NUnit constraint/classic form | MSTest |
|---|---|
| `Assert.That(actual, Is.EqualTo(expected))` for scalars | `Assert.AreEqual(expected, actual)` |
| `Assert.That(actual, Is.Not.EqualTo(expected))` | `Assert.AreNotEqual(expected, actual)` |
| `Assert.That(actual, Is.SameAs(expected))` | `Assert.AreSame(expected, actual)` |
| `Assert.That(actual, Is.Null)` | `Assert.IsNull(actual)` |
| `Assert.That(actual, Is.Not.Null)` | `Assert.IsNotNull(actual)` |
| `Assert.That(actual, Is.Empty)` | `Assert.IsEmpty(actual)` |
| `Assert.That(actual, Does.Contain(item))` | `Assert.Contains(item, actual)` |
| `Assert.That(actual, Is.EquivalentTo(expected))` | `CollectionAssert.AreEquivalent(expectedList, actualList)` |
| `Assert.That(actual, Is.EqualTo(expected))` for sequences | `Assert.AreSequenceEqual(expected, actual)` (MSTest 4.3+) or `CollectionAssert.AreEqual` |
NUnit `EqualConstraint` recursively compares enumerables. Determine whether operands are sequences before choosing `Assert.AreEqual`.
### Types and exceptions
| NUnit | MSTest |
|---|---|
| `Assert.That(value, Is.TypeOf<T>())` | `Assert.IsExactInstanceOfType<T>(value)` |
| `Assert.That(value, Is.InstanceOf<T>())` | `Assert.IsInstanceOfType<T>(value)` |
| `Assert.Throws<T>(...)` | `Assert.ThrowsExactly<T>(...)` |
| `Assert.That(code, Throws.TypeOf<T>())` | `Assert.ThrowsExactly<T>(...)` |
| `Assert.Catch<T>(...)` | `Assert.Throws<T>(...)` |
| `Assert.That(code, Throws.InstanceOf<T>())` | `Assert.Throws<T>(...)` |
| `Assert.DoesNotThrow(...)` | Execute directly; optionally catch and `Assert.Fail` when a clearer failure message is needed |
NUnit `Assert.Throws<T>` requires the exact exception type; `Assert.Catch<T>` accepts derived types. Do not map both to the same MSTest method.
### Constraint rewrites
| NUnit | MSTest |
|---|---|
| `Is.True` / `Is.False` | `Assert.IsTrue` / `Assert.IsFalse` |
| `Is.GreaterThan(x)` / `Is.LessThan(x)` | `Assert.IsGreaterThan(x, actual)` / `Assert.IsLessThan(x, actual)` |
| `Is.InRange(a, b)` | `Assert.IsInRange(actual, a, b)` |
| `Does.Contain`, `Does.StartWith`, `Does.EndWith` | corresponding MSTest `Assert` string/collection method |
| `Does.Match(regex)` | `Assert.MatchesRegex(regex, actual)` |
| `Has.Count.EqualTo(n)` | `Assert.HasCount(n, collection)` |
| property, `Some`, `All`, `Exactly`, `And`, `Or`, or custom constraints | Manual rewrite into explicit assertions without dropping any predicate |
`Assert.Multiple` has no behavior-identical built-in aggregation mapping. Sequential asserts change failure aggregation; use an agreed assertion library/helper or report the behavior change.
### Floating-point tolerance
`[DefaultFloatingPointTolerance(delta)]` has no MSTest attribute equivalent. Rewrite every affected `float` or `double` equality to an overload with an explicit delta. Respect NUnit's precedence: assertion-level `.Within(...)` overrides method, method overrides fixture, and fixture overrides assembly. Do not apply the delta to integer, decimal, or non-equality assertions.
## 4. Lifecycle and fixture instances
| NUnit | MSTest |
|---|---|
| `[SetUp]` | `[TestInitialize]` |
| `[TearDown]` | `[TestCleanup]` |
| `[OneTimeSetUp]` | static `[ClassInitialize]` accepting `TestContext` |
| `[OneTimeTearDown]` | static `[ClassCleanup]` |
| `[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]` | Remove attribute; MSTest already constructs a class instance per test |
| default `LifeCycle.SingleInstance` | Audit and explicitly preserve any class-shared state through static fields/lifecycle |
| `[SetUpFixture]` for the entire assembly | `[AssemblyInitialize]` / `[AssemblyCleanup]` |
| namespace-scoped `[SetUpFixture]` | Per-class lifecycle or shared helper; assembly lifecycle widens scope |
NUnit default fixture construction is the opposite of MSTest:
- NUnit default: one fixture instance shared across test cases.
- MSTest: a new test-class instance for each test.
Constructor-created mutable fields, lazy caches, counters, and objects disposed by one-time teardown require deliberate conversion. Do not assume setup attributes alone preserve them.
NUnit runs base setup before derived setup and derived teardown before base teardown. Preserve inheritance unless tests prove a safe flattening.
## 5. TestContext
| NUnit | MSTest |
|---|---|
| `TestContext.WriteLine(...)` | injected/property `TestContext.WriteLine(...)` |
| `TestContext.Out.WriteLine(...)` | `TestContext.WriteLine(...)` |
| `TestContext.Progress.WriteLine(...)` | `TestContext.WriteLine(...)`; output stream behavior may differ |
| `TestContext.CurrentContext.Test.Name` | `TestContext.TestName` |
| `TestContext.CurrentContext.Test.FullName` | combine `FullyQualifiedTestClassName` and `TestName` when needed |
| `TestContext.CurrentContext.WorkDirectory` | `TestContext.TestRunDirectory` after confirming intended directory semantics |
| `TestContext.AddTestAttachment(path)` | `TestContext.AddResultFile(path)` |
| `TestContext.CurrentContext.CancellationToken` | `TestContext.CancellationToken` |
Do not store MSTest `TestContext` in a static field.
## 6. Execution control
| NUnit | MSTest |
|---|---|
| `[Timeout(ms)]` | `[Timeout(ms)]` |
| `[CancelAfter(ms)]` on a VSTest method | `[Timeout(ms, CooperativeCancellation = true)]`; remove NUnit's injected token parameter and use `TestContext.CancellationToken` |
| fixture `[CancelAfter(ms)]` | Apply cooperative timeout to each affected test and relevant lifecycle method; verify setup/cleanup token flow |
| `[Retry(n)]` | `[Retry(n - 1)]` when `n > 1`; remove when `n = 1` |
| `[Repeat(n)]` | No direct equivalent; a loop is not identical because it produces one result and stops differently |
| `[MaxTime(ms)]` | Run code with timing and assert the elapsed threshold; not equivalent to timeout |
| `Assert.Pass()` | normally return from the test; review code after the call because NUnit stops execution immediately |
| `Assert.Ignore(reason)` | `Assert.Inconclusive(reason)` is the nearest runtime skip but reporting differs |
| `Assert.Inconclusive(reason)` | `Assert.Inconclusive(reason)` |
NUnit's retry count includes the first execution; MSTest's count is the number of retries after the first execution. NUnit `[Retry(3)]` therefore maps to MSTest `[Retry(2)]`. NUnit `RetryExceptions` filtering has no direct built-in mapping; use a custom retry attribute or report manual follow-up.
MSTest runner integration differs for cooperative token parameters. The VSTest adapter treats a `CancellationToken` method parameter as ordinary data and fails discovery/execution unless a data source supplies it. For preserved VSTest projects, use `TestContext.CancellationToken`. Retain a method token only after a probe proves the preserved MTP configuration injects it. NUnit uses `TestContext.CurrentContext.CancellationToken` in setup and teardown; map those uses to the current MSTest `TestContext.CancellationToken`.
## 7. Threading, platform, and culture
### Apartment and dedicated-thread attributes
| NUnit | MSTest |
|---|---|
| method `[Apartment(ApartmentState.STA)]` | replace `[TestMethod]` with `[STATestMethod]`; set `UseSTASynchronizationContext = true` when async continuations require the STA thread |
| fixture `[Apartment(ApartmentState.STA)]` | `[STATestClass]` |
| assembly `[Apartment(ApartmentState.STA)]` | apply `[STATestClass]` to every affected class; MSTest has no assembly-level STA attribute |
| `[Apartment(ApartmentState.MTA)]` | MSTest default, unless it overrides an inherited/source STA policy |
| `[RequiresThread]` | No direct mapping: it guarantees a fresh thread even when apartment state already matches |
| `[SingleThreaded]` | No direct mapping: `[DoNotParallelize]` serializes but does not guarantee one thread across class initialization, tests, and cleanup |
For `RequiresThread` or `SingleThreaded`, first determine whether tests observe thread identity, thread-local state, COM ownership, or synchronization context. Use a custom `TestMethodAttribute`/executor or a deliberately managed thread when identity matters. `STATestClass` is sufficient only when the real requirement is STA, not merely "same thread."
### Platform and culture gates
| NUnit | MSTest |
|---|---|
| `[Platform]` OS values | `[OSCondition]` with `ConditionMode.Include` or `Exclude` |
| `[Platform]` process architecture values | `[ArchitectureCondition]` |
| legacy OS versions, runtime family/version, or mixed platform expressions | custom `[MemberCondition]` helper that returns the same Boolean decision |
| `[Culture]` include/exclude | custom `[MemberCondition]` helper over `CultureInfo.CurrentCulture`, preserving neutral-culture matching |
Do not replace conditional attributes with `[Ignore]`; NUnit evaluates them per environment, while `Ignore` is unconditional. Preserve the skip reason where the target condition API supports one.
### Culture mutation
`[SetCulture]` and `[SetUICulture]` change state for the duration of a method, fixture, or assembly scope and restore it afterward. MSTest has no direct attributes.
- Save the original `CultureInfo.CurrentCulture` and/or `CurrentUICulture`.
- Set the requested value in constructor/`TestInitialize`, class initialization, or assembly initialization matching the source scope.
- Restore it in the corresponding cleanup, including failure paths.
- Do not combine unrelated culture scopes into one global setting.
- Review parallel execution: culture mutation must not leak to sibling tests or tasks.
## 8. Parallelization and ordering
| NUnit | MSTest |
|---|---|
| no parallel attributes | no `[assembly: Parallelize]` |
| `[assembly: Parallelizable(ParallelScope.Fixtures)]` | `[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]` |
| `[assembly: LevelOfParallelism(N)]` | `Workers = N` on `[assembly: Parallelize]` |
| `[NonParallelizable]` | `[DoNotParallelize]` when assembly parallelization is enabled |
| fixture `[Parallelizable(ParallelScope.Children)]` | method-level parallelism only after fixture-state safety review |
| shared named resource | MSTest 4.4 `[ResourceLock("name")]` can preserve a narrower serialization boundary |
| `[Order(n)]` | No direct general ordering equivalent |
MSTest 4.4 `[DependsOn]` expresses prerequisites, not arbitrary ordering. Use it only when a test truly depends on another test's successful completion.
## 9. Packages
Remove NUnit packages and adapters that are no longer used:
- `NUnit`
- `NUnit3TestAdapter`
- `NUnit.Analyzers`
- `NUnit.ConsoleRunner`
- `NUnit.Console`
- NUnit-specific MTP adapter packages
Keep framework-agnostic assertion, mocking, snapshot, and fixture libraries. Replace NUnit integrations such as `Verify.NUnit` with their MSTest counterpart when available.
Resolve the current stable MSTest v4 version from configured sources. For VSTest, preserve a compatible explicit `Microsoft.NET.Test.Sdk` pin when the source project owns one. For MTP, prefer `MSTest.Sdk` or configure the metapackage with `EnableMSTestRunner=true` and executable output.