|
| 1 | +--- |
| 2 | +name: tunit-test-runner |
| 3 | +description: >- |
| 4 | + Run, filter, and select TUnit tests through `dotnet test`. Use this whenever |
| 5 | + executing .NET tests in a project that depends on TUnit (built on |
| 6 | + Microsoft.Testing.Platform / MTP, not VSTest), when `dotnet test --filter` |
| 7 | + reports "Zero tests ran", or when tests must be narrowed by assembly, |
| 8 | + namespace, class, test name, [Category], or other custom properties. Covers |
| 9 | + the `--treenode-filter` path-based query syntax, its operators, the `--` |
| 10 | + separator rules across SDK versions, and common 0-tests troubleshooting. |
| 11 | +license: MIT |
| 12 | +--- |
| 13 | + |
| 14 | +# Running TUnit tests with `dotnet test` |
| 15 | + |
| 16 | +## The one rule that matters most |
| 17 | + |
| 18 | +TUnit runs on **Microsoft.Testing.Platform (MTP)**, not VSTest. The reflexive |
| 19 | +`dotnet test --filter "Category=X"` **does not work**: MTP silently rejects the |
| 20 | +`--filter` flag, prints its own help text, and exits with `Zero tests ran`. That |
| 21 | +looks like a passing-but-empty run or a config failure, but it is just an |
| 22 | +unrecognised flag. **Never reach for `--filter` on a TUnit project.** Use |
| 23 | +`--treenode-filter` instead. |
| 24 | + |
| 25 | +| Other frameworks (VSTest) | TUnit (MTP) | |
| 26 | +| ------------------------------------------ | ---------------------------------------------------- | |
| 27 | +| `--filter "Category=Integration"` | `--treenode-filter "/*/*/*/*[Category=Integration]"` | |
| 28 | +| `--filter "FullyQualifiedName~LoginTests"` | `--treenode-filter "/*/*/LoginTests/*"` | |
| 29 | +| `--filter "Name=AcceptCookiesTest"` | `--treenode-filter "/*/*/*/AcceptCookiesTest"` | |
| 30 | + |
| 31 | +## How to invoke it |
| 32 | + |
| 33 | +Prefer `dotnet test` over `dotnet run`: `dotnet test` builds and runs every |
| 34 | +targeted TFM automatically and works against a `.csproj`, `.sln`, or `.slnx`, |
| 35 | +whereas `dotnet run` only runs a single TFM. |
| 36 | + |
| 37 | +The catch is the `--` separator, which depends on the SDK: |
| 38 | + |
| 39 | +```bash |
| 40 | +# Universal form — works on every SDK. Use this by default. |
| 41 | +dotnet test -- --treenode-filter "/*/*/LoginTests/*" |
| 42 | + |
| 43 | +# .NET 10+ SDK only — the platform flag can be passed directly. |
| 44 | +dotnet test --treenode-filter "/*/*/LoginTests/*" |
| 45 | +``` |
| 46 | + |
| 47 | +Anything after `--` is passed through to the TUnit test runner rather than to |
| 48 | +the `dotnet test` command itself. Flags from extension packages |
| 49 | +(`--coverage`, `--report-trx`, `--results-directory`, etc.) **must** also sit |
| 50 | +after the `--`: |
| 51 | + |
| 52 | +```bash |
| 53 | +dotnet test --configuration Release --no-build \ |
| 54 | + -- --treenode-filter "/*/*/*/*[Category=Unit]" --coverage --report-trx |
| 55 | +``` |
| 56 | + |
| 57 | +> Run with no filter to execute everything: `dotnet test`. |
| 58 | +
|
| 59 | +## The `--treenode-filter` syntax |
| 60 | + |
| 61 | +A filter is a path with four segments, optionally annotated with a property |
| 62 | +group on any segment: |
| 63 | + |
| 64 | +``` |
| 65 | +/<Assembly>/<Namespace>/<Class>/<TestName>[Property=Value] |
| 66 | +``` |
| 67 | + |
| 68 | +Use `*` as a wildcard in any segment. The classic "run all tests" filter is |
| 69 | +`/*/*/*/*` — four wildcards, one per level. |
| 70 | + |
| 71 | +### Operators |
| 72 | + |
| 73 | +| Operator | Meaning | Example | |
| 74 | +| -------- | --------------------------------------------- | -------------------------------------- | |
| 75 | +| `*` | Wildcard within a segment | `/*/*/LoginTests*/*` | |
| 76 | +| `=` | Property equals (exact) | `/*/*/*/*[Category=Unit]` | |
| 77 | +| `!=` | Property not equal (exclude) | `/*/*/*/*[Category!=Slow]` | |
| 78 | +| `&` | AND — within one segment / property group | `/**[(Category=Unit)&(Priority=High)]` | |
| 79 | +| `\|` | OR — within one segment / property group | `/*/*/(LoginTests)\|(SignupTests)/*` | |
| 80 | +| `**` | Match any path depth (must be at the **end**) | `/MyAssembly/**` | |
| 81 | + |
| 82 | +### Two grammar rules that are easy to get wrong |
| 83 | + |
| 84 | +1. **`&` and `|` operate _inside a single segment or property group_, and each |
| 85 | + side must be wrapped in parentheses.** They do not join two complete paths. |
| 86 | +2. **Only one property group `[...]` is allowed per path segment.** Combine |
| 87 | + conditions _inside_ the single bracket — do not chain brackets. |
| 88 | +3. **`**` must terminate the path.** `/MyAssembly/**` is valid; `/**/Class/*` |
| 89 | + is not. |
| 90 | + |
| 91 | +## Troubleshooting: "Zero tests ran" / 0 tests discovered |
| 92 | + |
| 93 | +Check, in order: |
| 94 | + |
| 95 | +1. **`--filter` was used instead of `--treenode-filter`.** This is the most common cause. |
| 96 | +2. **`Microsoft.NET.Test.Sdk` is still referenced.** It conflicts with the TUnit MTP platform. |
| 97 | +3. **TUnit package missing.** Ensure `<PackageReference Include="TUnit" Version="*" />`. |
| 98 | +4. **Missing `[Test]` attribute** or unsupported method shape. |
| 99 | +5. **Wrong `OutputType`.** A `hostfxr.dll could not be found` error means the project needs `<OutputType>Exe</OutputType>`. |
| 100 | +6. **Bad filter shape.** Remember the path has exactly four segments. |
| 101 | + |
| 102 | +## References |
| 103 | + |
| 104 | +- TUnit — Test Filters: https://tunit.dev/docs/execution/test-filters/ |
| 105 | +- TUnit — Troubleshooting & FAQ: https://tunit.dev/docs/troubleshooting/ |
| 106 | +- TUnit — CI/CD pipelines: https://tunit.dev/docs/examples/tunit-ci-pipeline/ |
| 107 | +- TUnit — Explicit tests: https://tunit.dev/docs/writing-tests/explicit/ |
| 108 | +- MTP graph-query filtering spec: https://github.com/microsoft/testfx/blob/main/docs/mstest-runner-graphqueryfiltering/graph-query-filtering.md |
0 commit comments