VSTest -> Microsoft.Testing.Platform Migration
Migrate a .NET test solution from VSTest to Microsoft.Testing.Platform (MTP). The outcome is a solution where all test projects run on MTP,
works correctly, and CI/CD pipelines are updated.
Important: Do not mix VSTest-based and MTP-based .NET test projects in the same solution or run configuration -- this is an unsupported scenario.
When to Use
- Switching from VSTest to Microsoft.Testing.Platform for any supported test framework
- Enabling / / direct executable execution for test projects
- Enabling Native AOT or trimmed test execution
- Replacing with on MTP
- Updating CI/CD pipelines from the VSTest task to the .NET Core CLI task
- Updating arguments from VSTest syntax to MTP syntax
When Not to Use
- The project already runs on Microsoft.Testing.Platform and there is no remaining MTP behavioral difference to resolve (e.g., exit code 8 for zero tests discovered)
- Migrating between test frameworks (e.g., MSTest to xUnit.net) -- different effort entirely
- The project builds UWP or packaged WinUI test projects -- MTP does not support these yet
- The solution mixes .NET and non-.NET test adapters (e.g., JavaScript or C++ adapters) -- VSTest is required
- Upgrading MSTest versions -- use
migrate-mstest-v1v2-to-v3
or
Inputs
| Input | Required | Description |
|---|
| Project or solution path | Yes | The , , or entry point containing test projects |
| Test framework | No | MSTest, NUnit, xUnit.net v2, or xUnit.net v3. Auto-detected from package references |
| .NET SDK version | No | Determines integration mode. Auto-detected via |
| CI/CD pipeline files | No | Paths to pipeline definitions that invoke or |
Workflow
Step 1: Assess the solution
- Identify the test framework for each test project -- see the skill for the package-to-framework mapping. Key indicators:
- MSTest: References or , or uses (with not set to ). Note: alone is a library dependency, not a test project.
- NUnit: References
- xUnit.net: References and
xunit.runner.visualstudio
- Check the .NET SDK version () -- this determines how integrates with MTP
- Check whether a file exists at the solution or repo root -- all MTP properties should go there for consistency
- Check for usage in CI scripts or pipeline definitions
- Check for VSTest-specific arguments in CI scripts: , , , ,
- Run to establish a baseline of test pass/fail counts
Step 2: Set up Directory.Build.props
Critical: Set MTP runner properties in
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
sets this automatically. For all other setups (MSTest NuGet packages with
, NUnit with
, xUnit.net with
), prefer setting
<OutputType>Exe</OutputType>
centrally in
with a condition that targets only test projects. If you cannot reliably target only test projects from
, setting
<OutputType>Exe</OutputType>
per-project is an acceptable exception.
Conditioning in : Do NOT use
Condition="'$(IsTestProject)' == 'true'"
--
is set by the test SDK targets later in evaluation and is not available when
is imported. Use a property that is available early, such as
, to target test projects by naming convention. For example, if all test projects end in
:
xml
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
<OutputType>Exe</OutputType>
</PropertyGroup>
Adjust the condition (e.g.,
,
) 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
for consistency.
MSTest
Option A -- MSTest NuGet packages (3.2.0+):
xml
<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
, MTP is enabled by default -- no
or
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
- Update to 5.0.0+:
xml
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
- Enable the NUnit runner:
xml
<PropertyGroup>
<EnableNUnitRunner>true</EnableNUnitRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>
xUnit.net
Add a reference to
-- this package provides MTP support for xUnit.net v2 projects without requiring an upgrade to xunit.v3. You must also set
to
:
xml
<PackageReference Include="YTest.MTP.XUnit2" Version="0.4.0" />
xml
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
Note:
preserves the VSTest
syntax, so no filter migration is needed for xUnit.net v2. It also supports
for runsettings (xunit-specific configurations only),
, TRX reporting via
, and
.
xUnit.net v3
xUnit.net v3 (
package) has built-in MTP support. Enable it with:
xml
<PropertyGroup>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
Important: xUnit.net v3 on MTP does NOT support the VSTest
syntax. You must translate filters to xUnit.net v3's native filter options (see Step 5).
Step 4: Configure dotnet test integration
The
integration depends on the .NET SDK version.
.NET 10 SDK and later (recommended)
Use the native MTP mode by adding a
section to
:
json
{
"sdk": {
"version": "10.0.100"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
In this mode,
arguments are passed directly -- for example,
.
Important:
does not support trailing commas. Ensure the JSON is strictly valid.
.NET 9 SDK and earlier
Use the VSTest mode of
command to run MTP test projects by adding this property in
:
xml
<PropertyGroup>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>
Important: In this mode, you must use
to separate
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 (
,
,
,
,
, etc.) are unchanged.
| VSTest argument | MTP equivalent | Notes |
|---|
| Not applicable | MTP does not use external adapter discovery |
| Not applicable | |
| | Requires Microsoft.Testing.Extensions.CrashDump
NuGet package |
--blame-crash-dump-type <TYPE>
| | Requires CrashDump extension |
| | Requires Microsoft.Testing.Extensions.HangDump
NuGet package |
--blame-hang-dump-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 |
| | |
| | Same syntax for MSTest, NUnit, and xUnit.net v2 (with ). For xUnit.net v3, see filter migration below |
| | Requires Microsoft.Testing.Extensions.TrxReport
NuGet package |
--results-directory <DIR>
| --results-directory <DIR>
| Same |
| | MSTest and NUnit still support |
| | Same |
| | Applicable only to MSTest and NUnit |
Filter migration
MSTest, NUnit, and xUnit.net v2 (with ): The VSTest
syntax is identical on both VSTest and MTP. No changes needed.
xUnit.net v3 (native MTP): xUnit.net v3 does NOT support the VSTest
syntax on MTP. You must translate filters to xUnit.net v3's native filter options.
xUnit.net v3 filter flags
| Flag | Description |
|---|
| Run all tests in a given class. Supports wildcards (). |
--filter-not-class "name"
| Exclude all tests in a given class |
| 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:
.
VSTest → xUnit.net v3 filter translation table
| VSTest 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 |
| --filter-method *MethodName*
| Wildcards for substring match |
| (trait) | --filter-trait "Category=Value"
| Filter by trait name/value pair |
| Complex expressions | | Uses xUnit.net query filter language (see below) |
xUnit.net v3 query filter language
For complex expressions, use
with a path-segment syntax:
text
/<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
shell
# 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
and
, both conditions must match (AND behavior). For complex expressions, use
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:
xml
<!-- 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 (): Replace with the .NET Core CLI task (
):
yaml
# 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:
yaml
- task: DotNetCoreCLI@2
displayName: Run tests
inputs:
command: 'test'
arguments: '--no-build -- --report-trx --results-directory $(Agent.TempDirectory)'
GitHub Actions
Update
invocations in workflow files with the same argument translations from Step 5.
Replace vstest.console.exe
If any script invokes
directly, replace it with
. 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 when running tests
- Add to :
xml
<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:
- -- 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 )
- 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
- Run -- confirm zero errors
- Run -- 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
Validation
Common Pitfalls
| Pitfall | Solution |
|---|
| Mixing VSTest and MTP projects in the same solution | Migrate all test projects together -- mixed mode is unsupported |
| arguments ignored on .NET 9 and earlier | Use to separate build args from MTP args: dotnet test -- --report-trx
|
| Exit code 8 on CI without failures | MTP fails when zero tests run; use or fix test discovery |
| MSTest.Sdk v4 + vstest.console no longer works | MSTest.Sdk v4 no longer adds -- add it explicitly or switch to |
Missing <OutputType>Exe</OutputType>
| Required for all setups except MSTest.Sdk (which sets it automatically) |
Using Condition="'$(IsTestProject)' == 'true'"
in | is not yet defined when is evaluated -- use $(MSBuildProjectName.EndsWith('.Tests'))
(or a similar name-based check) instead |
Next Steps
- Use for running tests on the new MTP platform
- Use for iterative test fixing with hot reload on MTP
More Info