# How to Migrate Between Test Frameworks Like xUnit to MSTest or VSTest to MTP

> Easily migrate .NET test projects between xUnit MSTest and VSTest. Learn to replace packages, remap attributes, convert fixtures, and validate test parity for seamless transitions.

- Repository: [.NET Platform/skills](https://github.com/dotnet/skills)
- Tags: migration-guide
- Published: 2026-07-07

---

**Migrating .NET test projects requires replacing NuGet packages, remapping attributes and assertions, converting fixtures to lifecycle hooks, and validating test parity to ensure identical behavior across xUnit, MSTest, and the Microsoft Testing Platform.**

The `dotnet/skills` repository provides authoritative migration guidance through dedicated skills that automate the transition between popular .NET testing frameworks. Whether standardizing on MSTest v4 or modernizing from the classic VSTest runner to the new Microsoft Testing Platform (MTP), systematic migration prevents silent behavioral changes in parallelization and exception handling.

## Understanding Architectural Differences

Before modifying code, recognize how each framework handles discovery, execution, and lifecycle management. According to the migration skills in `dotnet/skills`, the most significant behavioral divergences involve **parallelization defaults** and **exception assertion semantics**.

| Aspect | xUnit | MSTest v4 | VSTest | MTP |
|--------|-------|-----------|--------|-----|
| **Discovery & Execution** | Uses `xunit.runner.visualstudio` (VSTest) or `xunit.v3.mtp-v*` (MTP) | Uses `MSTest` metapackage or `MSTest.Sdk` | Classic `Microsoft.NET.Test.Sdk` | New `Microsoft.Testing.Platform` engine |
| **Parallelization** | Parallel across classes, serial inside class | Serial by default; requires `[assembly: Parallelize]` | Serial (unless overridden) | Mirrors MSTest defaults |
| **Lifecycle Hooks** | Constructors, `IAsyncLifetime`, `IClassFixture<T>`, `ICollectionFixture<T>` | Constructors, `IDisposable`, `[TestInitialize]`, `[TestCleanup]`, `[ClassInitialize]`, `[ClassCleanup]` | Same as MSTest | Same as MSTest |
| **Data-Driven Tests** | `[InlineData]`, `[MemberData]`, `[ClassData]` | `[DataRow]`, `[DynamicData]` | Same as MSTest | Same as MSTest |
| **Exact Exception Types** | `Assert.Throws<T>` requires exact type | `Assert.ThrowsExactly<T>` for exact type, `Assert.Throws<T>` for derived types | Same as MSTest | Same as MSTest |

Failing to address these differences—particularly parallelization and exception assertions—can cause tests to pass silently or become flaky after migration.

## Migrating from xUnit to MSTest

The skill defined in [`plugins/dotnet-test-migration/skills/migrate-xunit-to-mstest/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-test-migration/skills/migrate-xunit-to-mstest/SKILL.md) outlines a five-phase migration: assess, replace packages, rewrite code, adjust metadata, and verify.

### Package Replacement

Remove all xUnit packages and introduce the MSTest metapackage. In your `.csproj` file:

```xml
<!-- Remove xUnit packages -->
<ItemGroup>
  <PackageReference Remove="xunit" />
  <PackageReference Remove="xunit.assert" />
  <PackageReference Remove="xunit.runner.visualstudio" />
</ItemGroup>

<!-- Add MSTest -->
<ItemGroup>
  <PackageReference Include="MSTest" Version="4.1.0" />
</ItemGroup>

```

Alternatively, use `MSTest.Sdk` as the project SDK with `<UseVSTest>true</UseVSTest>` if maintaining VSTest compatibility during transition.

### Attribute Mapping

Replace xUnit attributes with their MSTest equivalents as documented in [`references/mapping-cheatsheet.md`](https://github.com/dotnet/skills/blob/main/references/mapping-cheatsheet.md):

| xUnit | MSTest |
|-------|--------|
| `[Fact]` | `[TestMethod]` |
| `[Theory]` | `[TestMethod]` (with `[DataRow]` or `[DynamicData]`) |
| `[Fact(Skip="...")]` | `[Ignore("...")]` |
| `[Trait("Category","Unit")]` | `[TestCategory("Unit")]` |
| `[Trait("Owner","alice")]` | `[TestProperty("Owner","alice")]` |

Convert theory tests to data-driven tests:

```csharp
// xUnit
[Theory]
[InlineData(1, true)]
[InlineData(2, false)]
public void IsEven(int n, bool expected) => Assert.Equal(expected, n % 2 == 0);

```

```csharp
// MSTest
[DataTestMethod]
[DataRow(1, true)]
[DataRow(2, false)]
public void IsEven(int n, bool expected)
{
    Assert.AreEqual(expected, n % 2 == 0);
}

```

### Assertion Conversions

Update assertions to match MSTest semantics. Critical differences include exact type matching:

| xUnit | MSTest |
|-------|--------|
| `Assert.Equal(a, b)` | `Assert.AreEqual(a, b)` |
| `Assert.Throws<T>(action)` | `Assert.ThrowsExactly<T>(action)` |
| `Assert.ThrowsAny<T>(action)` | `Assert.Throws<T>(action)` |

```csharp
// xUnit
Assert.Throws<InvalidOperationException>(() => service.DoWork());

// MSTest
Assert.ThrowsExactly<InvalidOperationException>(() => service.DoWork());

```

### Fixture and Lifecycle Migration

Convert `IClassFixture<T>` to static fields with `[ClassInitialize]` and `[ClassCleanup]`:

```csharp
// xUnit
public class DatabaseFixture : IDisposable { /* ... */ }

public class RepoTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _db;
    public RepoTests(DatabaseFixture db) => _db = db;
}

```

```csharp
// MSTest
[TestClass]
public sealed class RepoTests
{
    private static DatabaseFixture? _db;

    [ClassInitialize]
    public static void Init(TestContext _) => _db = new DatabaseFixture();

    [ClassCleanup]
    public static void Cleanup() => _db?.Dispose();
}

```

For `ICollectionFixture<T>`, either hoist to `[AssemblyInitialize]` (if scope widening is acceptable) or maintain per-class static fixtures with `[DoNotParallelize]` to preserve serialization.

Replace `ITestOutputHelper` with `TestContext`:

```csharp
// xUnit
public MyTests(ITestOutputHelper output) => _out = output;

// MSTest
public MyTests(TestContext ctx) => _testContext = ctx;

```

### Parallelization Configuration

xUnit runs test classes in parallel by default. MSTest requires explicit configuration. Add to any `.cs` file at assembly scope:

```csharp
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]

```

Omit this attribute to maintain serial execution (MSTest's default).

## Migrating from VSTest to Microsoft Testing Platform (MTP)

The skill in [`plugins/dotnet-test-migration/skills/migrate-vstest-to-mtp/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-test-migration/skills/migrate-vstest-to-mtp/SKILL.md) handles runner transitions without changing the underlying test framework.

### SDK and Project File Changes

Switch from the classic test SDK to the Microsoft Testing Platform SDK:

```xml
<!-- Before (VSTest) -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
    <PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
  </ItemGroup>
</Project>

<!-- After (MTP) -->
<Project Sdk="Microsoft.Testing.Platform.Sdk/1.0.0">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
  </PropertyGroup>
</Project>

```

Remove explicit `Microsoft.NET.Test.Sdk` references when using the MTP SDK, as the platform pulls the correct runtime automatically. Preserve parallelization settings using `[Parallelize]` attributes, as MTP respects MSTest's parallelization configuration.

## Validation and Verification

After migration, execute the validation checklist from the SKILL file to ensure behavioral parity:

```bash
dotnet build               # Must succeed without warnings

dotnet test --no-build     # Should discover identical test count

```

Compare the pass/fail counts against the baseline recorded during initial assessment. Pay special attention to tests using exception assertions or relying on specific parallelization behavior, as these are the most common sources of migration-induced failures.

## Summary

- **Assess first** using the platform-detection logic in `dotnet/skills` to identify current packages and target frameworks.
- **Replace packages** by removing xUnit or VSTest SDK references and adding `MSTest` 4.1+ or `Microsoft.Testing.Platform.Sdk`.
- **Remap attributes** using `[TestMethod]`, `[DataRow]`, and `[TestCategory]` instead of `[Fact]`, `[InlineData]`, and `[Trait]`.
- **Convert assertions** to `Assert.ThrowsExactly<T>` for exact type matching and `Assert.AreEqual` for equality checks.
- **Migrate fixtures** from `IClassFixture<T>` to static fields with `[ClassInitialize]`/`[ClassCleanup]`.
- **Configure parallelization** explicitly with `[Parallelize]` to match xUnit's default class-level parallelism.
- **Validate** by comparing test counts and results before and after migration.

## Frequently Asked Questions

### What is the difference between MSTest and Microsoft Testing Platform?

**MSTest is the test framework** (attributes, assertions, lifecycle), while the **Microsoft Testing Platform (MTP) is the test runner** (discovery, execution, reporting). You can run MSTest tests using either the classic VSTest runner or the new MTP runner. The migration from VSTest to MTP specifically swaps the execution engine while keeping your MSTest code unchanged.

### Do I need to change all my test classes when switching from xUnit to MSTest?

**Yes, you must update attributes and assertions**, but constructors and basic structure remain similar. You need to replace `[Fact]` with `[TestMethod]`, convert `[Theory]` to `[DataTestMethod]` with `[DataRow]`, and update `using` statements from `Xunit` to `Microsoft.VisualStudio.TestTools.UnitTesting`. The `dotnet/skills` repository provides a mapping cheatsheet at [`references/mapping-cheatsheet.md`](https://github.com/dotnet/skills/blob/main/references/mapping-cheatsheet.md) to automate these conversions.

### Will my tests run in parallel after migrating to MSTest?

**Not by default.** MSTest runs tests serially unless you explicitly add `[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]` to an assembly-level file. This is the opposite of xUnit's default behavior, which parallelizes across classes. If your tests rely on parallel execution for performance, you must add this attribute during migration.

### Can I migrate gradually or must I convert all projects at once?

**You can migrate incrementally.** The `Microsoft.NET.Test.Sdk` supports multiple test frameworks in the same solution. However, when migrating from VSTest to MTP, you must choose the runner at the project level—individual projects can use different runners within the same solution. The `dotnet/skills` agents support analyzing and converting projects one at a time to manage risk.