dotnet test
Run test projects locally and in CI, filter by trait or name, produce reports and coverage, and read exit codes in VSTest and MTP modes.
dotnet test [<project|solution>] [-c <cfg>] [--no-build] [--filter <expr>] [--logger <logger>] [--results-directory <dir>] [--collect <collector>] [--blame-hang-timeout <time>] [-- <runner args>]- Added in .NET .NET Core 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$project|solutionoptional | path= the project or solution in the current folder | Test project, solution or folder. In Microsoft.Testing.Platform mode use --project or --solution instead. |
$-c, --configurationoptional | string= Debug | Build configuration of the tests and the code under test. |
$--no-buildoptional | flag= false | Run existing build output; the configuration must match the earlier build. |
$--filteroptional | expression= all tests | VSTest mode: select tests by property, e.g. Category=Integration or FullyQualifiedName~Orders.Api. |
$--loggeroptional | string= console | VSTest mode: extra result loggers, e.g. trx or "console;verbosity=detailed". |
$--results-directoryoptional | path= ./TestResults | Where TRX files, coverage and blame dumps are written. |
$--collectoptional | string | VSTest mode: data collector, e.g. "XPlat Code Coverage" (coverlet.collector). |
$--blame-hang-timeoutoptional | time span | VSTest mode: abort and dump the test host when one test runs longer than this, e.g. 5m. |
Return value
process exit code0 when all selected tests pass. Non-zero when a test fails, the build fails or the run aborts; in Microsoft.Testing.Platform mode 2 means failed tests and 8 means no tests ran.
Errors
A failing build returns 1 before any test runs. In VSTest mode a filter that matches nothing only prints a warning; in MTP mode it fails with exit code 8.
Changelog
- PHP .NET 10dotnet test can run in Microsoft.Testing.Platform mode, selected in global.json, where test projects run as their own executables.
dotnet test builds the test projects and then runs them. How it runs them depends on the mode, and a team must know which one it is in, because the options differ.
Two modes
| VSTest (default) | Microsoft.Testing.Platform (MTP) | |
|---|---|---|
| How tests run | vstest.console loads an adapter into a test host process | Each test project is an executable that runs its own tests |
| Project needs | Microsoft.NET.Test.Sdk plus an adapter such as xunit.runner.visualstudio | A framework with an MTP runner, e.g. xunit.v3 with UseMicrosoftTestingPlatformRunner |
| Select by | --filter "Category=Integration" | The framework's own options, e.g. xUnit v3 --filter-trait "Category=Integration" |
| Target | Positional path | --project or --solution |
| No tests matched | Warning, exit 0 | Exit code 8 |
MTP mode is switched on for the whole repository in global.json ("test": { "runner": "Microsoft.Testing.Platform" }), so every project in the solution must support it. Choose MTP for new xUnit v3 solutions: fewer packages, and dotnet run on a test project works for debugging.
Filters
VSTest filter expressions combine Name, FullyQualifiedName, ClassName and traits with =, !=, ~ (contains), & and |. In xUnit a trait is [Trait("Category", "Integration")]. Quote the expression in the shell: an unquoted | becomes a pipe. The dangerous failure is a filter typo: in VSTest mode Category=Intergration runs zero tests and the build is green. MTP's exit code 8 exists to stop exactly that.
Reports, coverage and hangs
--logger trx --results-directory artifacts/test-resultswrites TRX files for the CI test tab (VSTest). In MTP mode the framework provides report options, such as xUnit v3's--report-xunit-trx.--collect "XPlat Code Coverage"uses thecoverlet.collectorpackage and writes Cobertura XML per project; merge and threshold it in a later step.--blame-hang-timeout 5mkills a test host stuck on one test and collects a dump, instead of the job hanging until the CI timeout.
Configuration and --no-build
Like build, test defaults to Debug. dotnet build -c Release followed by dotnet test --no-build looks in bin/Debug. Pass the same -c to both. Integration tests with WebApplicationFactory also need the API project built in that configuration, which a solution-level build guarantees.
Parallelism across projects
Test projects in a solution run in parallel with each other. Two projects that both start the same Testcontainers database on a fixed port, or write to the same file, collide only on some runs. Give each its own container or resources, or run them in one project.
Example
using Xunit;
public sealed record OrderLine(string Sku, int Quantity, decimal UnitPrice);
public static class InvoiceCalculator
{
public static decimal Total(IEnumerable<OrderLine> lines, decimal vatRate)
{
var net = 0m;
foreach (var line in lines)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(line.Quantity, nameof(lines));
net += line.Quantity * line.UnitPrice;
}
return Math.Round(net * (1 + vatRate), 2, MidpointRounding.AwayFromZero);
}
}
// dotnet test --filter "Category=Unit" (VSTest mode)
// dotnet test -- --filter-trait "Category=Unit" (MTP mode, xUnit v3 option)
[Trait("Category", "Unit")]
public sealed class InvoiceCalculatorTests
{
[Fact]
public void Adds_vat_and_rounds_half_away_from_zero()
{
var lines = new[] { new OrderLine("KETTLE-BLUE", 1, 34.50m), new OrderLine("MUG-WHITE", 3, 8.00m) };
Assert.Equal(70.20m, InvoiceCalculator.Total(lines, 0.20m));
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Rejects_a_line_with_zero_or_negative_quantity(int quantity)
{
var lines = new[] { new OrderLine("MUG-WHITE", quantity, 8.00m) };
Assert.Throws<ArgumentOutOfRangeException>(() => InvoiceCalculator.Total(lines, 0.20m));
}
}
// Excluded from the fast PR run with --filter "Category!=Integration".
[Trait("Category", "Integration")]
public sealed class InvoiceStorageTests
{
[Fact]
public async Task Stores_an_invoice_number_sequence()
{
var ct = TestContext.Current.CancellationToken;
await Task.Delay(10, ct);
Assert.Equal("INV-000042", $"INV-{42:D6}");
}
}When to use it
- Run only Category=Unit tests on every pull request and the full suite, including Testcontainers tests, on main
- Publish TRX results and Cobertura coverage from dotnet test as CI artifacts for the test and coverage tabs
- Abort a test run with --blame-hang-timeout 5m and collect a dump when a checkout test deadlocks
Frequently asked questions
Why did dotnet test --no-build say no test output was found?
The build used a different configuration. dotnet build -c Release writes bin/Release, while dotnet test defaults to Debug. Pass -c Release to dotnet test as well.
Which runner mode am I in?
Check global.json: a test section with runner Microsoft.Testing.Platform means MTP mode, otherwise VSTest. In MTP mode --filter and --logger are not available; use the test framework's own options.
How do I fail the build when a filter selects no tests?
In MTP mode it already fails with exit code 8. In VSTest mode, check the TRX or console summary for a non-zero total in a script, or move to MTP.
More examples
CI steps in VSTest mode
Build once, test with <code>--no-build</code> and the same <code>-c Release</code>. <code>if: always()</code> uploads TRX files and hang dumps even when tests fail, which is exactly when they are needed.
name: test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- run: dotnet restore Shop.slnx --locked-mode
- run: dotnet build Shop.slnx -c Release --no-restore
- name: Unit and integration tests
run: >
dotnet test Shop.slnx -c Release --no-build
--logger trx --results-directory artifacts/test-results
--collect "XPlat Code Coverage"
--blame-hang-timeout 5m
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: artifacts/test-resultsThe same run in MTP mode
In MTP mode the selection and report options come from the test framework, here xUnit v3, so <code>--filter</code> expressions from VSTest do not apply. The exit code tells a script the difference between failing tests and a filter that matched nothing.
# global.json contains: "test": { "runner": "Microsoft.Testing.Platform" }
# Whole solution; options for the test apps follow the CLI options.
dotnet test --solution Shop.slnx -c Release --no-build
# One project, only unit tests, with an xUnit TRX report.
dotnet test --project tests/Orders.UnitTests -c Release --no-build \
--filter-trait "Category=Unit" --report-xunit-trx
# A test project is an executable: debug one test without any runner.
dotnet run --project tests/Orders.UnitTests -- --filter-method "*Adds_vat*"
echo "exit code: $?" # 0 passed, 2 failures, 8 no tests matchedFilters that mean what you think
Trait names and values are case-sensitive in practice and typos are silent. Printing test names, or asserting a minimum test count in CI, catches the filter that quietly stopped matching after a refactor renamed a trait.
# Quote expressions: | and & are shell operators.
dotnet test --filter "Category=Unit|Category=Contract"
# Contains (~) on the fully qualified name: one namespace.
dotnet test --filter "FullyQualifiedName~Orders.Api.Tests.Checkout"
# Everything except slow tests.
dotnet test --filter "Category!=Integration&Category!=Slow"
# Detailed console output shows each test name, so a zero-test run is visible.
dotnet test --filter "Category=Unit" --logger "console;verbosity=normal"
Discussion