Platform Upgrades & Migration v0.1.0

VSTest -> Microsoft.Testing.Platform Migration

Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing MTP properties and OutputType=Exe on test projects via MSBuildProjectName, not IsTestProject. Supports MSTest, NUnit, xUnit.net v2 (via YTest.MTP.XUnit2), and xUnit.net v3. Covers runner enablement, CLI argument and filter translation (--filter-class/--filter-trait/--filter-query), global.json config, CI/CD updates, and extension packages. DO NOT USE FOR: migrating between test frameworks (MSTest/xUnit/NUnit), xUnit.net v2 to v3 API migration, MSTest version upgrades, TFM upgrades, or UWP/WinUI test projects.

Workflow

Step 1: Assess the solution

  1. Identify the test framework for each test project -- see the platform-detection skill for the package-to-framework mapping. Key indicators:

- MSTest: References MSTest or MSTest.TestAdapter, or uses MSTest.Sdk (with <IsTestApplication> not set to false). Note: MSTest.TestFramework alone is a library dependency, not a test project. - NUnit: References NUnit3TestAdapter - xUnit.net: References xunit and xunit.runner.visualstudio

  1. Check the .NET SDK version (dotnet --version) -- this determines how dotnet test integrates with MTP
  2. Check whether a Directory.Build.props file exists at the solution or repo root -- all MTP properties should go there for consistency
  3. Check for vstest.console.exe usage in CI scripts or pipeline definitions
  4. Check for VSTest-specific dotnet test arguments in CI scripts: --filter, --logger, --collect, --settings, --blame*
  5. Run dotnet test to establish a baseline of test pass/fail counts

Step 2: Set up Directory.Build.props

> Critical: Set MTP runner properties in Directory.Build.props at the solution or repo root whenever possible, rather than per-project. This prevents inconsistent configuration where some projects use VSTest and others use MTP (an unsupported scenario). > Note: MTP also requires test projects to have <OutputType>Exe</OutputType>. Only MSTest.Sdk sets this automatically. For all other setups (MSTest NuGet packages with EnableMSTestRunner, NUnit with EnableNUnitRunner, xUnit.net with YTest.MTP.XUnit2), prefer setting <OutputType>Exe</OutputType> centrally in Directory.Build.props with a condition that targets only test projects. If you cannot reliably target only test projects from Directory.Build.props, setting <OutputType>Exe</OutputType> per-project is an acceptable exception. > > Conditioning in `Directory.Build.props`: Do NOT use Condition="'$(IsTestProject)' == 'true'" -- IsTestProject is set by the test SDK targets later in evaluation and is not available when Directory.Build.props is imported. Use a property that is available early, such as MSBuildProjectName, to target test projects by naming convention. For example, if all test projects end in .Tests: > > ``xml > <PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))"> > <OutputType>Exe</OutputType> > </PropertyGroup> > ` > > Adjust the condition (e.g., .EndsWith('Tests'), .Contains('.Test')`) to match the test project naming convention used in the repository.

Step 3: Enable the framework-specific MTP runner

Each framework has its own opt-in property. Add these in Directory.Build.props for consistency.

#### MSTest

Option A -- MSTest NuGet packages (3.2.0+):

<PropertyGroup>
  <EnableMSTestRunner>true</EnableMSTestRunner>
  <OutputType>Exe</OutputType>
</PropertyGroup>

Ensure the project references MSTest 3.2.0 or later. If the version is already 3.2.0+, no MSTest version upgrade is needed for MTP migration.

Option B -- MSTest.Sdk:

When using MSTest.Sdk, MTP is enabled by default -- no EnableMSTestRunner or OutputType Exe property is needed (the SDK sets both automatically). The only action is: if the project has <UseVSTest>true</UseVSTest>, remove it. That property forces the project to use VSTest instead of MTP.

#### NUnit

Requires NUnit3TestAdapter 5.0.0 or later.

  1. Update NUnit3TestAdapter to 5.0.0+:
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
  1. Enable the NUnit runner:
<PropertyGroup>
  <EnableNUnitRunner>true</EnableNUnitRunner>
  <OutputType>Exe</OutputType>
</PropertyGroup>

#### xUnit.net

Add a reference to YTest.MTP.XUnit2 -- this package provides MTP support for xUnit.net v2 projects without requiring an upgrade to xunit.v3. You must also set OutputType to Exe:

<PackageReference Include="YTest.MTP.XUnit2" Version="0.4.0" />
<PropertyGroup>
  <OutputType>Exe</OutputType>
</PropertyGroup>

> Note: YTest.MTP.XUnit2 preserves the VSTest --filter syntax, so no filter migration is needed for xUnit.net v2. It also supports --settings for runsettings (xunit-specific configurations only), xunit.runner.json, TRX reporting via --report-trx, and --treenode-filter.

#### xUnit.net v3

xUnit.net v3 (xunit.v3 package) has built-in MTP support. Enable it with:

<PropertyGroup>
  <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>

> Important: xUnit.net v3 on MTP does NOT support the VSTest --filter syntax. You must translate filters to xUnit.net v3's native filter options (see Step 5).

Step 4: Configure dotnet test integration

The dotnet test integration depends on the .NET SDK version.

#### .NET 10 SDK and later (recommended)

Use the native MTP mode by adding a test section to global.json:

{
  "sdk": {
    "version": "10.0.100"
  },
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

In this mode, dotnet test arguments are passed directly -- for example, dotnet test --report-trx.

> Important: global.json does not support trailing commas. Ensure the JSON is strictly valid.

#### .NET 9 SDK and earlier

Use the VSTest mode of dotnet test command to run MTP test projects by adding this property in Directory.Build.props:

<PropertyGroup>
  <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>

> Important: In this mode, you must use -- to separate dotnet test build arguments from MTP arguments. For example: dotnet test --no-build -- --list-tests.

Step 5: Update dotnet test command-line arguments

VSTest-specific arguments must be translated to MTP equivalents. Build-related arguments (-c, -f, --no-build, --nologo, -v, etc.) are unchanged.

| VSTest argument | MTP equivalent | Notes | |-----------------|----------------|-------| | --test-adapter-path | Not applicable | MTP does not use external adapter discovery | | --blame | Not applicable | | | --blame-crash | --crashdump | Requires Microsoft.Testing.Extensions.CrashDump NuGet package | | --blame-crash-dump-type <TYPE> | --crashdump-type <TYPE> | Requires CrashDump extension | | --blame-hang | --hangdump | Requires Microsoft.Testing.Extensions.HangDump NuGet package | | --blame-hang-dump-type <TYPE> | --hangdump-type <TYPE> | Requires HangDump extension | | --blame-hang-timeout <TIMESPAN> | --hangdump-timeout <TIMESPAN> | Requires HangDump extension | | --collect "Code Coverage;Format=cobertura" | --coverage --coverage-output-format cobertura | Per-extension arguments | | -d\|--diag <LOG_FILE> | --diagnostic | | | --filter <EXPRESSION> | --filter <EXPRESSION> | Same syntax for MSTest, NUnit, and xUnit.net v2 (with YTest.MTP.XUnit2). For xUnit.net v3, see filter migration below | | -l\|--logger trx | --report-trx | Requires Microsoft.Testing.Extensions.TrxReport NuGet package | | --results-directory <DIR> | --results-directory <DIR> | Same | | -s\|--settings <FILE> | --settings <FILE> | MSTest and NUnit still support .runsettings | | -t\|--list-tests | --list-tests | Same | | -- <RunSettings args> | --test-parameter | Applicable only to MSTest and NUnit |

#### Filter migration

MSTest, NUnit, and xUnit.net v2 (with `YTest.MTP.XUnit2`): The VSTest --filter syntax is identical on both VSTest and MTP. No changes needed.

xUnit.net v3 (native MTP): xUnit.net v3 does NOT support the VSTest --filter syntax on MTP. You must translate filters to xUnit.net v3's native filter options.

#### xUnit.net v3 filter flags

| Flag | Description | |------|-------------| | --filter-class "name" | Run all tests in a given class. Supports wildcards (*). | | --filter-not-class "name" | Exclude all tests in a given class | | --filter-method "name" | Run a specific test method | | --filter-not-method "name" | Exclude a specific test method | | --filter-namespace "name" | Run all tests in a namespace | | --filter-not-namespace "name" | Exclude all tests in a namespace | | --filter-trait "name=value" | Run tests with a matching trait | | --filter-not-trait "name=value" | Exclude tests with a matching trait |

Multiple values can be specified with a single flag: --filter-class Foo Bar.

#### VSTest → xUnit.net v3 filter translation table

| VSTest --filter syntax | xUnit.net v3 MTP equivalent | Notes | |---|---|---| | FullyQualifiedName~ClassName | --filter-class *ClassName* | Wildcards required for substring match | | FullyQualifiedName=Ns.Class.Method | --filter-method Ns.Class.Method | Exact match on fully qualified method | | Name=MethodName | --filter-method *MethodName* | Wildcards for substring match | | Category=Value (trait) | --filter-trait "Category=Value" | Filter by trait name/value pair | | Complex expressions | --filter-query "expr" | Uses xUnit.net query filter language (see below) |

#### xUnit.net v3 query filter language

For complex expressions, use --filter-query with a path-segment syntax:

/<assemblyFilter>/<namespaceFilter>/<classFilter>/<methodFilter>[traitName=traitValue]

Each segment matches against: assembly name, namespace, class name, method name. Use * for "match all" in any segment. Documentation: <https://xunit.net/docs/query-filter-language>

#### Translation example

# VSTest
dotnet test --filter "FullyQualifiedName~IntegrationTests&Category=Smoke"

# xUnit.net v3 MTP -- using individual filters (AND behavior)
dotnet test -- --filter-class *IntegrationTests* --filter-trait "Category=Smoke"

# xUnit.net v3 MTP -- using query language (assembly/namespace/class/method[trait])
dotnet test -- --filter-query "/*/*/*IntegrationTests*/*[Category=Smoke]"

> Note: When combining --filter-class and --filter-trait, both conditions must match (AND behavior). For complex expressions, use --filter-query with the path-segment syntax. See the xUnit.net query filter language docs for full reference.

Step 6: Install MTP extension packages (if needed)

If CI scripts use TRX reporting, crash dumps, or hang dumps, add the corresponding NuGet packages:

<!-- TRX report generation (replaces --logger trx) -->
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="1.6.2" />

<!-- Crash dump collection (replaces --blame-crash) -->
<PackageReference Include="Microsoft.Testing.Extensions.CrashDump" Version="1.6.2" />

<!-- Hang dump collection (replaces --blame-hang) -->
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" Version="1.6.2" />

<!-- Code coverage (replaces --collect "Code Coverage") -->
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="17.13.0" />

Step 7: Update CI/CD pipelines

#### Azure DevOps

If using the VSTest task (`VSTest@3`): Replace with the .NET Core CLI task (DotNetCoreCLI@2):

# Before (VSTest task)
- task: VSTest@3
  inputs:
    testAssemblyVer2: '**/*Tests.dll'
    runSettingsFile: 'test.runsettings'

# After (.NET Core CLI task)
- task: DotNetCoreCLI@2
  displayName: Run tests
  inputs:
    command: 'test'
    arguments: '--no-build --configuration Release'

If already using DotNetCoreCLI@2: Update arguments per Step 5 translations. Remember the -- separator on .NET 9 and earlier:

- task: DotNetCoreCLI@2
  displayName: Run tests
  inputs:
    command: 'test'
    arguments: '--no-build -- --report-trx --results-directory $(Agent.TempDirectory)'

#### GitHub Actions

Update dotnet test invocations in workflow files with the same argument translations from Step 5.

#### Replace vstest.console.exe

If any script invokes vstest.console.exe directly, replace it with dotnet test. The test projects are now executables and can also be run directly.

Step 8: Handle behavioral differences

#### Zero tests exit code

VSTest silently succeeds when zero tests are discovered. MTP fails with exit code 8. Options:

  • Pass --ignore-exit-code 8 when running tests
  • Add to Directory.Build.props:
<PropertyGroup>
  <TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
</PropertyGroup>
  • Use environment variable: TESTINGPLATFORM_EXITCODE_IGNORE=8

Step 9: Remove VSTest-only packages (optional)

Once migration is complete and verified, remove packages that are only needed for VSTest:

  • Microsoft.NET.Test.Sdk -- not needed for MTP (MSTest.Sdk v4 already omits it by default)
  • xunit.runner.visualstudio -- only needed for VSTest discovery of xUnit.net (not needed when using YTest.MTP.XUnit2)
  • NUnit3TestAdapter VSTest-only features -- the adapter is still needed but only for the MTP runner

> Note: If you need to maintain VSTest compatibility during a transition period, keep these packages.

Step 10: Verify

  1. Run dotnet build -- confirm zero errors
  2. Run dotnet test -- confirm all tests pass
  3. Compare test pass/fail counts to the pre-migration baseline
  4. Run the test executable directly (e.g., ./bin/Debug/net8.0/MyTests.exe) -- confirm it works
  5. Verify CI pipeline produces the expected test result artifacts (TRX files, code coverage, crash dumps)
  6. Test that Test Explorer in Visual Studio (17.14+) or VS Code discovers and runs tests

Related skills