The kit’s test suites run with the standard dotnet test command. Three layers, three different speed profiles, one set of commands.
Run everything
dotnet test src/FSH.Starter.slnxThis runs every test project. Expect:
- Unit projects: sub-second each
- Architecture project: ~1-2 seconds
- Integration project: 30-60 seconds (cold Testcontainers) / ~5-10 seconds (warm)
Run a single project
# Unit tests for one moduledotnet test src/Tests/Identity.Tests/
# Architecture tests only — fast, no containersdotnet test src/Tests/Architecture.Tests/
# Integration tests only — Docker must be runningdotnet test src/Tests/Integration.Tests/Filter by name
# Single test classdotnet test --filter "FullyQualifiedName~ProductsEndpointTests"
# Single test methoddotnet test --filter "FullyQualifiedName~ProductsEndpointTests.POST_products_Should_ReturnCreated_And_PersistProduct_When_PayloadIsValid"
# By trait or categorydotnet test --filter "Category=Integration"The ~ operator does substring match; = does exact match. Combine with & and |:
dotnet test --filter "FullyQualifiedName~Catalog & FullyQualifiedName~Product"Parallelism
xUnit runs test classes in parallel within a single project by default. Tests within a class run sequentially unless marked otherwise.
To disable parallelism across classes (e.g. for shared state):
# xunit.runner.json in the test project root{ "parallelizeTestCollections": false}The kit’s integration project uses class-level parallelism with shared fixtures — IClassFixture<FshWebApplicationFactory> shares the factory across tests in a class; xUnit runs classes in parallel where the fixtures permit.
Verbose output
# Detailed output — useful for debugging slow testsdotnet test --logger "console;verbosity=detailed"
# Trx output for CI parsingdotnet test --logger "trx;LogFileName=test-results.trx"
# Bothdotnet test --logger "console;verbosity=detailed" --logger "trx"Code coverage
The kit ships coverlet.collector in unit projects. Generate a coverage report:
dotnet test src/Tests/Identity.Tests/ --collect:"XPlat Code Coverage"
# Generates a .cobertura.xml file under TestResults/# Convert to HTML with the reportgenerator tool:dotnet tool install --global dotnet-reportgenerator-globaltoolreportgenerator -reports:**/coverage.cobertura.xml -targetdir:coveragereportThe HTML report lands in coveragereport/index.html. Coverage targets in the kit hover around 75-85% for unit projects; integration tests push effective coverage higher.
Docker requirement for integration tests
Testcontainers needs a running Docker daemon. Locally, Docker Desktop on macOS / Windows, Docker Engine on Linux. In CI, the runner image must have Docker available — GitHub-hosted runners do; self-hosted runners need explicit setup.
# Verify Docker is reachable before running integration testsdocker infoIf Docker isn’t available, integration tests fail with Testcontainers.DockerNotAvailableException.
CI — GitHub Actions
A minimal workflow:
name: tests
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Restore run: dotnet restore src/FSH.Starter.slnx - name: Build run: dotnet build src/FSH.Starter.slnx --no-restore -c Release - name: Test (unit + architecture) run: dotnet test src/FSH.Starter.slnx --no-build -c Release --filter "FullyQualifiedName!~Integration.Tests" --logger "trx;LogFileName=test-results.trx" - name: Test (integration) run: dotnet test src/Tests/Integration.Tests/ --no-build -c Release --logger "trx;LogFileName=integration-results.trx" - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: '**/*.trx'GitHub-hosted runners (ubuntu-latest) have Docker pre-installed, so Testcontainers works without extra setup.
CI — speed optimisations
For repos that run tests on every push, the slow piece is integration tests. Three knobs:
- Skip integration on draft PRs. Run unit + architecture only on
pull_request: types: [opened, synchronize]and integration onpush: branches: [main]. - Cache the NuGet packages.
actions/cacheagainst~/.nuget/packageskeyed by**/packages.lock.json. - Reuse the Testcontainers image cache. Cache
/var/lib/docker(advanced) or accept the first-run cold start (simpler).
For repos with many integration tests, shard them across multiple jobs:
strategy: matrix: shard: [1, 2, 3, 4]steps: - run: dotnet test src/Tests/Integration.Tests/ --filter "FullyQualifiedName~Shard${{ matrix.shard }}"Tag tests with [Trait("Shard", "1")] to assign them.
Watch mode (local iteration)
dotnet watch reruns tests on file change — useful while iterating on a single test:
dotnet watch test --project src/Tests/Identity.Tests/ \ -- --filter "FullyQualifiedName~RegisterUserCommandHandlerTests"It picks up file edits via dotnet-watch, recompiles, and reruns the filter. Fast feedback loop without the project’s startup cost on every change.
Container cleanup
Testcontainers spins up containers and keeps them running between test sessions in the same process. If something goes wrong (a test killed by Ctrl-C mid-run), containers can leak:
# List leaked Testcontainers containersdocker ps -a | grep testcontainers
# Stop themdocker stop $(docker ps -aq --filter "label=testcontainers")
# Remove themdocker rm $(docker ps -aq --filter "label=testcontainers")
# Or just everything Testcontainers-labelleddocker container prune --filter "label=testcontainers"Related
- Unit tests — what runs fastest.
- Integration tests — what runs slowest.
- Writing new tests — recipes for each layer.
- Fixtures & seed data — what the integration tests share.