dotnet test: VSTest and Microsoft.Testing.Platform
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its 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 |
|
Test project |
library + |
executable ( |
Run without |
|
the project’s own executable ( |
Filters |
|
framework options ( |
Reports |
|
extension options ( |
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 —
--loggerand--collectare 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).