dotnet test: VSTest and Microsoft.Testing.Platform

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

dotnet test builds test projects and runs their tests, but since .NET 10 it does so in one of two ways. VSTest, the test platform dotnet test has always used, is still the default. Microsoft.Testing.Platform (MTP) runs each test project as an ordinary executable and is opted into per repository. The two modes take different options and return different exit codes, so a CI script written for one breaks on the other. This page runs both with .NET SDK 10.0.402. Writing tests is covered in the C# Reference (Testing — Running Tests, The Microsoft Testing Platform).

Choosing the mode

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

test.runner in global.json switches every dotnet test under that folder to MTP mode; VSTest (or no setting) keeps VSTest. MTP mode needs the .NET 10 SDK and Microsoft.Testing.Platform 1.7 or later in every test project. From the .NET 11 previews, the DOTNET_TEST_RUNNER environment variable overrides the file.

Ledgerline uses MTP, because its test project uses xUnit v3 4.0.2, which runs on MTP v2. Run in VSTest mode — which is what happened before test.runner was added — the build stops:

~/.nuget/packages/microsoft.testing.platform.msbuild/2.5.1/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets(395,5): error : Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. If you use dotnet test, you should opt-in to the new dotnet test experience. For more information, see https://aka.ms/dotnet-test-mtp-error [~/ledgerline/tests/Ledgerline.Tests/Ledgerline.Tests.csproj]

The mode is per repository, not per project: a solution whose test projects mix MTP-only and VSTest-only frameworks has to move them together.

Microsoft.Testing.Platform mode

The Ledgerline test project is an executable that references the xUnit v3 packages:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="xunit.v3" />
    <PackageReference Include="xunit.runner.visualstudio" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Ledgerline\Ledgerline.csproj" />
  </ItemGroup>

</Project>
dotnet test
Running tests from ~/ledgerline/tests/Ledgerline.Tests/bin/Debug/net10.0/Ledgerline.Tests.dll (net10.0|x64)
~/ledgerline/tests/Ledgerline.Tests/bin/Debug/net10.0/Ledgerline.Tests.dll (net10.0|x64) passed (625ms)

Test run summary: Passed!
  total: 3
  failed: 0
  succeeded: 3
  skipped: 0
  duration: 898ms

The options come in three layers, all listed by dotnet test -h in MTP mode: the dotnet test options (--project, --solution, -c, -f, --no-build, --results-directory, --list-tests, --minimum-expected-tests, …), the platform options every MTP test application has (--timeout, --diagnostic, --zero-tests-policy, --filter-uid, …), and the extension options of the test framework — for xUnit v3, --filter-class, --filter-method, --filter-namespace, the VSTest-syntax --filter, and the reports --report-xunit-trx, --report-xunit-junit, --report-xunit-html, --report-xunit-ctrf and others. They are passed directly; the -- separator of older MTP versions is not needed.

dotnet test --no-build --list-tests
dotnet test --no-build --filter-method '*Trial_balance*'
dotnet test --no-build --report-xunit-trx --results-directory TestResults
  Ledgerline.Tests.LedgerTests.Parse_reads_every_data_line
  Ledgerline.Tests.LedgerTests.Balanced_journal_has_no_unbalanced_entries
  Ledgerline.Tests.LedgerTests.Trial_balance_sums_per_account

Discovered 3 tests.

Test run summary: Passed!
  total: 1
  failed: 0
  succeeded: 1
  skipped: 0
  duration: 866ms

    - ~/ledgerline/TestResults/root_vm_2026-10-23_19_10_39.582419.trx

A failing assertion shows the test, the message and the stack, and exits with 2:

failed Ledgerline.Tests.LedgerTests.Trial_balance_sums_per_account (2ms)
  from ~/ledgerline/tests/Ledgerline.Tests/bin/Debug/net10.0/Ledgerline.Tests.dll (net10.0|x64)
  Assert.Equal() Failure: Values differ
  Expected: 1
  Actual:   2
    at Ledgerline.Tests.LedgerTests.Trial_balance_sums_per_account() in ~/ledgerline/tests/Ledgerline.Tests/LedgerTests.cs:32
…
Test run summary: Failed!
  total: 3
  failed: 1
  succeeded: 2
  skipped: 0
  duration: 872ms
Test run completed with non-success exit code: 2 (see: https://aka.ms/testingplatform/exitcodes)

A filter that matches nothing is a failure in MTP mode — exit code 8:

dotnet test --no-build --filter-method 'Nope'; echo "exit=$?"
Test run summary: Zero tests ran
  error: 1

  total: 0
  failed: 0
  succeeded: 0
  skipped: 0
  duration: 634ms
Test run completed with non-success exit code: 8 (see: https://aka.ms/testingplatform/exitcodes)
exit=8

That catches a CI filter that silently stopped matching after a rename. --ignore-exit-code 8 or --zero-tests-policy turns it off for jobs where an empty selection is expected; --minimum-expected-tests <n> (exit code 9) is the stronger version of the same check.

VSTest mode

A project from the xunit template (xUnit v2 2.9.3, Microsoft.NET.Test.Sdk 17.14.1, xunit.runner.visualstudio 3.1.4), in a folder without test.runner:

dotnet test
dotnet test --no-build --filter "FullyQualifiedName~Test1" --logger "trx;LogFileName=vst.trx"
dotnet test --no-build --filter "FullyQualifiedName~Nope"; echo "exit=$?"
Test run for ~/vst/Vst.Tests/bin/Debug/net10.0/Vst.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 14 ms - Vst.Tests.dll (net10.0)
A total of 1 test files matched the specified pattern.
Results File: ~/vst/Vst.Tests/TestResults/vst.trx

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 11 ms - Vst.Tests.dll (net10.0)
A total of 1 test files matched the specified pattern.
No test matches the given testcase filter `FullyQualifiedName~Nope` in ~/vst/Vst.Tests/bin/Debug/net10.0/Vst.Tests.dll
exit=0

The VSTest filter syntax is <property><operator><value> with =, !=, ~ (contains) and !~, combined with | and &; the properties are FullyQualifiedName, Name, ClassName, Priority and TestCategory (or the framework’s trait names). Results come from loggers (--logger trx, --logger "console;verbosity=detailed"), and --collect "XPlat Code Coverage" runs the Coverlet collector the template adds.

VSTest has only two exit codes: 0 when every test passed — including when no test ran, as above — and 1 when a test failed or the run did not complete.

dotnet vstest (running a test DLL directly) is legacy; dotnet test <dll> forwards to the same engine.

Comparison

VSTest Microsoft.Testing.Platform

Selected by

default

global.json test.runner (DOTNET_TEST_RUNNER from .NET 11)

Test project

library + Microsoft.NET.Test.Sdk + adapter

executable (OutputType=Exe) with the framework’s MTP runner

Run without dotnet test

dotnet vstest / vstest.console

the project’s own executable (./Ledgerline.Tests)

Filters

--filter expressions

framework options (--filter-method, …); xUnit v3 also accepts --filter

Reports

--logger

extension options (--report-xunit-trx, --report-trx with the TRX extension)

Zero tests

exit 0

exit 8

Failed tests

exit 1

exit 2

MSTest offers a project SDK, MSTest.Sdk, that configures MTP, the runner and recommended extensions in one line (<Project Sdk="MSTest.Sdk/4.0.2">). The SDK 10.0.402 mstest template still uses the MSTest 4.0.2 metapackage in a Microsoft.NET.Sdk project instead.

Common mistakes

  • Upgrading to xUnit v3 4.x or another MTP-v2 framework without test.runner — "Testing with VSTest target is no longer supported".

  • Keeping VSTest options after switching — --logger and --collect are VSTest options; MTP uses extension options.

  • A filter that matches nothing — silently green in VSTest, exit 8 in MTP.

  • CI scripts that treat any non-zero exit as "tests failed" — MTP uses distinct codes (2 failures, 8 zero tests, 9 minimum not met).