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.

Signature
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

ParameterTypeDescription
$project|solutionoptionalpath= the project or solution in the current folderTest project, solution or folder. In Microsoft.Testing.Platform mode use --project or --solution instead.
$-c, --configurationoptionalstring= DebugBuild configuration of the tests and the code under test.
$--no-buildoptionalflag= falseRun existing build output; the configuration must match the earlier build.
$--filteroptionalexpression= all testsVSTest mode: select tests by property, e.g. Category=Integration or FullyQualifiedName~Orders.Api.
$--loggeroptionalstring= consoleVSTest mode: extra result loggers, e.g. trx or "console;verbosity=detailed".
$--results-directoryoptionalpath= ./TestResultsWhere TRX files, coverage and blame dumps are written.
$--collectoptionalstringVSTest mode: data collector, e.g. "XPlat Code Coverage" (coverlet.collector).
$--blame-hang-timeoutoptionaltime spanVSTest 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 runvstest.console loads an adapter into a test host processEach test project is an executable that runs its own tests
Project needsMicrosoft.NET.Test.Sdk plus an adapter such as xunit.runner.visualstudioA 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"
TargetPositional path--project or --solution
No tests matchedWarning, exit 0Exit 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-results writes 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 the coverlet.collector package and writes Cobertura XML per project; merge and threshold it in a later step.
  • --blame-hang-timeout 5m kills 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

Example · csharp
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.

Example · yaml
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-results

The 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.

Example · bash
# 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 matched

Filters 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.

Example · bash
# 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

  • Be the first to comment on this lesson.