Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.
.claude/skills/dotnet-running-unit-tests/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 178% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 86% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 98% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 121% | 0% |
This repo uses xUnit v3 with the Microsoft.Testing.Platform (MTP) runner. Test projects are built as OutputType=Exe, so each test assembly is a self-contained host process — not a classic VSTest assembly.
There are two ways to run tests:
| Method | When to use | |--------|-------------| | dotnet test <project> | Dev loop — run a single test project, optionally filtered | | build.cmd -test / build.sh --test | Final validation — builds everything and runs all test projects via the repo's Arcade harness |
Use a fast, scoped dotnet test loop while iterating, and the full build.cmd -test pass before declaring the change done.
These are configured in Directory.Build.props (repo root), src/Directory.Build.targets, and src/Shared/UnitTests/xunit.runner.json. They apply to both dotnet test and build.cmd -test unless noted otherwise:
dotnet test runs the suite once per TFM.xunit.runner.json sets maxParallelThreads: 1 and parallelizeTestCollections: false. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.--filter-not-trait Category=... (e.g., nonwindowstests, failing, nonnetcoreapptests). Don't try to "fix" tests that appear skipped because of these.--coverage --coverage-settings Coverage.config is appended unconditionally to XunitOptions in src/Directory.Build.targets. There is no MSBuild property switch to disable it from dotnet test — to skip coverage, run the test exe directly without --coverage.TestRunnerName=XUnitV3, MTP 1.9.1, xUnit v3 3.2.2 (set in repo-root Directory.Build.props).RunnerUtilities.GetMSBuildEnvironmentVariables (in src/UnitTests.Shared/RunnerUtilities.cs) sets DOTNET_HOST_PATH to the bootstrap dotnet when tests launch MSBuild as a child process, so tasks like RoslynCodeTaskFactory resolve the right host. Don't override DOTNET_HOST_PATH from a test.This repo uses MTP with the xUnit v3 MTP runner, not VSTest. Many familiar dotnet test flags silently do nothing. Key differences:
--report-trx (not --logger trx), --coverage (not --collect "XPlat Code Coverage"), --filter-method/--filter-class/--filter-trait for xUnit v3 native filtering.--filter, --nologo, --blame, --settings *.runsettings, --diag, --collect, or -- RunConfiguration.MaxCpuCount=... — these are VSTest-only and ignored.-c, -f, --no-restore, --no-build, -v, -bl:, -p:Property=Value — these are interpreted by dotnet test itself before MTP sees them.For comprehensive MTP vs VSTest flag reference, see the run-tests skill from the dotnet-test plugin.
Aim for sub-30s iterations.
> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with dotnet (src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj); on Linux/macOS use / and shell line continuations (\), and the exe path becomes artifacts/bin/<Proj>/Debug/net11.0/<Proj> (no .exe).
dotnet test with a single TFM and filter (incremental build handles rebuilding automatically):powershell dotnet test src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj -f net11.0 -- --filter-method "MyFeature"
powershell dotnet build src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0 artifacts\bin\Microsoft.Build.Tasks.UnitTests\Debug\net11.0\Microsoft.Build.Tasks.UnitTests.exe --filter-method "MyFeature" --no-progress The exe path is TFM-specific: Debug\net11.0\...exe for net11.0, Debug\net472\...exe for net472. Switching TFM without rebuilding silently runs stale binaries.
These trade safety for speed — use during iteration, revert before final validation:
-- Single TFM: pass -f net11.0. Halves runtime on Windows by skipping net472.
xunit.runner.json next to the test project (or override the existing one) with:json { "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "longRunningTestSeconds": 60, "maxParallelThreads": -1, "parallelizeTestCollections": true } Or pass them as runner args after --: -- xUnit.MaxParallelThreads=-1 xUnit.ParallelizeTestCollections=true. Expect flakes in tests that touch env vars, cwd, the file system, or the global ProjectCollection — don't ship a fix that depends on this being on.
dotnet build ... -p:CreateBootstrap=false (useful when the local bootstrap SDK payload is missing).
--filter-trait Category=mytraitduringdev if you've tagged a focused subset.--coverage. This repo does not expose a supported dotnet test property switch to disable the auto-added coverage arguments.Before saying "done," run the heavy configuration. Don't skip TFMs and don't keep parallel-overrides.
xunit.runner.json change).powershell dotnet test src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj -c Release dotnet test src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj -c Release # Add other UnitTests projects whose code paths you touched
powershell .\build.cmd -test -c Release # Windows ./build.sh --test -c Release # Linux/macOS
powershell dotnet test <project> -c Release --report-trx --report-trx-filename validation.trx --results-directory artifacts\TestResults
MTP's final summary reports passed, failed, and skipped separately. Always check the skipped count — platform-conditional attributes (WindowsOnlyFact, UnixOnlyFact, etc.) and the auto trait filters cause expected skips, but a sudden jump in skipped count can hide a regression where a test became inapplicable on the current platform without you intending it.
Match the source area you changed to its *.UnitTests project:
| Source area | Test project | |-------------|--------------| | src/Build/** (engine, evaluation, backend) | src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj | | src/Framework/** | src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj | | src/Tasks/** | src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj | | src/Utilities/** | src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj | | src/MSBuild/** (CLI) | src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj | | src/Build/BuildCheck/** | src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.csproj | | src/Shared/** | Run the consumers above (Build, Tasks, Utilities) — shared code is linked into all of them. |
| Scenario | Command | |----------|---------| | Fast scoped dev loop | dotnet test <proj> -f net11.0 -- --filter-method "*X*" | | Direct test exe | artifacts\bin\<Proj>\Debug\net11.0\<Proj>.exe --filter-method "*X*" --no-progress | | Single test by name | dotnet test <proj> -- --filter-method "*MyTestMethod*" | | Final per-project validation | dotnet test <proj> -c Release (all TFMs) | | Final full validation | .\build.cmd -test -c Release / ./build.sh --test -c Release | | TRX report | --report-trx --report-trx-filename out.trx --results-directory artifacts\TestResults |
.github/instructions/tests.instructions.md — test authoring conventions (xUnit v3, Shouldly, TestEnvironment, MockLogger).src/Shared/UnitTests/xunit.runner.json — repo-wide xUnit settings.src/Directory.Build.targets — XunitOptions, auto trait filters, coverage wiring.documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 8,778 | 11,328 | +29% | 1 | 1 | 0% | 1,321 | 3,674 | +178% | 0 | 0 | — |
case-02 | fail→pass | 10,179 | 5,176 | -49% | 1 | 1 | 0% | 1,888 | 3,520 | +86% | 0 | 0 | — |
case-03 | fail→pass | 14,033 | 8,510 | -39% | 1 | 1 | 0% | 2,570 | 4,151 | +62% | 0 | 0 | — |
case-04 | fail→pass | 7,865 | 3,180 | -60% | 1 | 1 | 0% | 1,463 | 2,896 | +98% | 0 | 0 | — |
case-05 | fail→pass | 8,190 | 2,964 | -64% | 1 | 1 | 0% | 1,331 | 2,944 | +121% | 0 | 0 | — |
case-06 | fail→pass | 7,896 | 2,971 | -62% | 1 | 1 | 0% | 1,251 | 2,907 | +132% | 0 | 0 | — |
case-07 | fail→pass | 10,631 | 2,642 | -75% | 1 | 1 | 0% | 1,844 | 2,902 | +57% | 0 | 0 | — |
case-08 | fail→pass | 7,980 | 4,924 | -38% | 1 | 1 | 0% | 1,549 | 3,294 | +113% | 0 | 0 | — |
case-09 | fail→pass | 11,753 | 4,920 | -58% | 1 | 1 | 0% | 1,899 | 3,389 | +78% | 0 | 0 | — |
case-10 | fail→pass | 14,958 | 3,065 | -80% | 1 | 1 | 0% | 2,143 | 2,954 | +38% | 0 | 0 | — |
case-11 | fail→pass | 12,931 | 6,093 | -53% | 1 | 1 | 0% | 2,123 | 3,279 | +54% | 0 | 0 | — |
case-12 | fail→pass | 16,598 | 5,268 | -68% | 1 | 1 | 0% | 2,358 | 3,429 | +45% | 0 | 0 | — |
case-13 | fail→pass | 7,000 | 3,143 | -55% | 1 | 1 | 0% | 1,199 | 3,067 | +156% | 0 | 0 | — |
case-14 | pass→pass | 17,232 | 5,813 | -66% | 1 | 1 | 0% | 2,886 | 3,358 | +16% | 0 | 0 | — |
case-15 | fail→pass | 14,046 | 6,104 | -57% | 1 | 1 | 0% | 2,074 | 3,603 | +74% | 0 | 0 | — |
case-16 | pass→pass | 13,376 | 5,058 | -62% | 1 | 1 | 0% | 1,890 | 3,292 | +74% | 0 | 0 | — |
case-17 | fail→pass | 12,695 | 4,612 | -64% | 1 | 1 | 0% | 2,218 | 3,061 | +38% | 0 | 0 | — |
case-18 | pass→pass | 13,363 | 3,612 | -73% | 1 | 1 | 0% | 2,063 | 3,087 | +50% | 0 | 0 | — |
case-19 | fail→pass | 12,218 | 5,419 | -56% | 1 | 1 | 0% | 1,852 | 3,291 | +78% | 0 | 0 | — |
case-20 | pass→pass | 11,298 | 12,952 | +15% | 1 | 1 | 0% | 2,096 | 4,910 | +134% | 0 | 0 | — |
case-21 | pass→pass | 20,354 | 14,296 | -30% | 1 | 1 | 0% | 3,007 | 5,091 | +69% | 0 | 0 | — |
case-22 | pass→pass | 10,380 | 9,533 | -8% | 1 | 1 | 0% | 1,564 | 3,871 | +148% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +73 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.