Migrate MSTest v1 or v2 test project to MSTest v3.
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
- Identify the test framework for each test project -- see the
platform-detectionskill 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
- Check the .NET SDK version (
dotnet --version) -- this determines howdotnet testintegrates with MTP - Check whether a
Directory.Build.propsfile exists at the solution or repo root -- all MTP properties should go there for consistency - Check for
vstest.console.exeusage in CI scripts or pipeline definitions - Check for VSTest-specific
dotnet testarguments in CI scripts:--filter,--logger,--collect,--settings,--blame* - Run
dotnet testto 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.
- Update
NUnit3TestAdapterto 5.0.0+:
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
- 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 8when 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 usingYTest.MTP.XUnit2)NUnit3TestAdapterVSTest-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
- Run
dotnet build-- confirm zero errors - Run
dotnet test-- confirm all tests pass - Compare test pass/fail counts to the pre-migration baseline
- Run the test executable directly (e.g.,
./bin/Debug/net8.0/MyTests.exe) -- confirm it works - Verify CI pipeline produces the expected test result artifacts (TRX files, code coverage, crash dumps)
- Test that Test Explorer in Visual Studio (17.14+) or VS Code discovers and runs tests
Related skills
Fix build errors and breaking changes after upgrading MSTest from v3 to v4, or plan a complete MSTest v3-to-v4 migration.
Migrate .NET test projects from xUnit.net (v2 or v3) to MSTest v4.