---
name: dotnet-testing
description: Design and write a .NET test strategy and the tests themselves — xUnit (or NUnit/MSTest) conventions, unit tests of domain logic, ASP.NET Core integration tests with WebApplicationFactory, real databases with Testcontainers, mocking with NSubstitute or Moq and when not to mock, test data builders, assertion libraries, contract and architecture tests, coverage with coverlet, flaky-test triage and CI. Use this whenever the user asks how to test a C# class, handler, controller, minimal API endpoint, DbContext or background service; creates a *.Tests.csproj; sets up WebApplicationFactory, Testcontainers or EF InMemory/SQLite; asks about mocking ILogger, HttpClient or DbContext, test coverage, dotnet test in CI, or "what should I test". Also apply it when reviewing .NET test code or generating tests for C# code the user just wrote, even if they don't say the word "test".
metadata:
  technology: .NET
  type: testing
---

# .NET Testing

Tests exist so the team can refactor and upgrade .NET versions without fear. Optimise for confidence per minute of maintenance: test behaviour through public seams with real infrastructure where it's cheap, and mock only what you don't own.

## 1. The pyramid (what to write, how much)

| Layer | Tooling | Share | Tests what |
|---|---|---|---|
| Static | Nullable + warnings-as-errors, .NET analyzers, `dotnet format` | Free | Null bugs, API misuse, style |
| Unit | xUnit + assertions | ~40% | Domain types, value objects, pure services, validators, mapping |
| Integration (in-process) | `WebApplicationFactory` + Testcontainers | ~45% | Endpoints through the real pipeline: routing, auth, validation, EF, serialization |
| Contract | Pact, or OpenAPI diff, or published message schemas | ~5% | Agreements with consumers/providers |
| Architecture | NetArchTest / ArchUnitNET | Few | Module and layer boundaries |
| End-to-end | Playwright/HTTP smoke against a deployed env | ~5–10% | A handful of critical journeys |

Push toward integration tests at the HTTP boundary for ASP.NET Core services: they catch DI, middleware, serialization and query bugs that unit tests with mocks cannot.

## 2. Framework conventions

- **xUnit** is the common default (a new instance per test, constructor = setup, `IDisposable`/`IAsyncLifetime` = teardown, `IClassFixture<T>`/collection fixtures for shared expensive state). NUnit and MSTest are fine — stay consistent with the repo.
- One test project per production project (`Orders.Tests`, `Orders.IntegrationTests`); mirror namespaces.
- Name tests as behaviour: `PlaceOrder_WithEmptyCart_ReturnsValidationProblem` or a sentence in `[Fact(DisplayName = ...)]`.
- Arrange / Act / Assert, one behaviour per test; `[Theory]` + `[InlineData]`/`[MemberData]` for input tables.
- Async tests return `Task`; pass `TestContext.Current.CancellationToken` (xUnit v3) or a timeout token to I/O.
- Inject `TimeProvider` (and use `FakeTimeProvider`) instead of `DateTime.UtcNow`; never `Thread.Sleep`.

## 3. Unit tests of domain logic

```csharp
[Fact]
public void Cancel_ShippedOrder_Throws()
{
    var order = new OrderBuilder().Shipped().Build();

    var act = () => order.Cancel("changed mind");

    act.ShouldThrow<DomainException>().Message.ShouldContain("shipped");
}
```

- Test invariants, state transitions and edge cases (boundaries, empty, null, max) of aggregates and value objects — no mocks needed if the domain is pure.
- Don't unit-test framework glue (endpoint mapping, EF configuration); integration tests cover it.

## 4. Integration tests with WebApplicationFactory

```csharp
public sealed class ApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _db = new PostgreSqlBuilder().WithImage("postgres:16-alpine").Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder) =>
        builder.UseEnvironment("Testing").ConfigureTestServices(services =>
        {
            services.RemoveAll<DbContextOptions<OrdersDbContext>>();
            services.AddDbContext<OrdersDbContext>(o => o.UseNpgsql(_db.GetConnectionString()));
        });

    public Task InitializeAsync() => _db.StartAsync();               // then apply migrations
    Task IAsyncLifetime.DisposeAsync() => _db.DisposeAsync().AsTask(); // xUnit v3 uses ValueTask
}
```

- Expose `Program` to tests (`public partial class Program;` or `InternalsVisibleTo`).
- Replace only true externals in `ConfigureTestServices`: payment gateways, email, message transport (use the broker library's test harness), clock.
- Auth: a test authentication handler issuing claims per test, rather than disabling authorisation.
- Assert on HTTP status, ProblemDetails body and resulting database state — not on internal calls.
- Reset data between tests (Respawn, transaction rollback, or a fresh schema per class); never depend on test order.

## 5. Which database for tests?

| Option | Use when | Trade-off |
|---|---|---|
| **Testcontainers** with the real engine (SQL Server, PostgreSQL) | Default for integration tests | Needs Docker in CI; slower startup — share one container per test run/collection |
| SQLite in-memory | Fast checks where SQL dialect doesn't matter | Different SQL, constraints and functions; false confidence on complex queries |
| EF Core InMemory provider | Almost never — Microsoft discourages it for testing | Not relational: no transactions, constraints or real LINQ translation |
| Repository/handler fakes | Pure unit tests of application logic | Doesn't test the query at all |

## 6. Mocking — and when not to

- Libraries: **NSubstitute** (terse) or **Moq** (widely used; its 2023 SponsorLink episode led some teams to switch — either is acceptable, follow the repo). FakeItEasy is also fine.
- Mock **what you don't own and can't run cheaply**: external HTTP APIs (prefer a stub `HttpMessageHandler` or WireMock.Net), email/SMS, clocks.
- **Don't mock**: `DbContext`/`DbSet` (use a real database), your own domain types, value objects, `IOptions<T>` (use `Options.Create`), `ILogger<T>` (use `NullLogger<T>` or `FakeLogger<T>` when asserting logs).
- Assert interactions only when the interaction *is* the behaviour (a message was published, an email sent) — otherwise assert outcomes.

## 7. Test data

- **Builders** with sensible defaults (`new OrderBuilder().WithLines(3).Build()`) keep tests readable and resilient to constructor changes.
- AutoFixture (optionally with `AutoNSubstitute`/`AutoMoq`) or Bogus for filling irrelevant data; keep values that matter explicit in the test.
- No shared mutable static fixtures; each test creates what it asserts on.

## 8. Assertions

- Built-in `Assert` is fine. Shouldly and FluentAssertions give better messages.
- **FluentAssertions v8+ is under a commercial licence** for commercial use (v7 and earlier remain Apache 2.0) — confirm licensing before adding or upgrading; Shouldly or built-in asserts are free alternatives.
- Compare objects structurally (`BeEquivalentTo`, records' value equality) rather than field-by-field noise.

## 9. Contract and architecture tests

- Consumer-driven contracts (PactNet) between services owned by different teams; for public APIs, diff the generated OpenAPI document in CI and fail on breaking changes.
- Snapshot-test message/JSON contracts (e.g. Verify) so accidental property renames fail.
- Architecture rules with **NetArchTest** or **ArchUnitNET**: `Domain` has no dependency on `Infrastructure`/EF, modules reference only other modules' `Contracts`, handlers are `internal sealed`.

## 10. Coverage, flakiness and CI

- Coverage via **coverlet** (`coverlet.collector` + `dotnet test --collect:"XPlat Code Coverage"`) or Microsoft's code coverage tooling; report with ReportGenerator. Gate on changed-lines coverage, not a global number ratcheting forever.
- CI shape: `dotnet build -warnaserror` → unit tests → integration tests (Docker available) → publish results (TRX/JUnit) and coverage.
- Run tests in Release in CI with `--no-build`; set `TZ`/culture explicitly (`CultureInfo.InvariantCulture`) to catch locale bugs.

| Flaky symptom | Likely cause | Fix |
|---|---|---|
| Passes alone, fails in suite | Shared DB rows or static state; parallel collections | Reset data per test, isolate collections, remove statics |
| Fails on CI only | Time zone/culture, slower machine, Docker startup | `FakeTimeProvider`, invariant culture, container wait strategies |
| Intermittent timeout | Real delays, un-awaited tasks, background services racing | Await everything, disable/replace hosted services in tests, poll with deadline |
| Order-dependent | Tests relying on data created by others | Each test arranges its own data |

## Deliverable format

When asked to write tests:
1. List the **behaviours** to cover (happy path, validation, authorisation, not found, concurrency/edge cases) before code.
2. State the **layer** chosen for each and why.
3. Provide complete test classes plus any fixture/builder additions, reusing existing factories and builders.
4. Give the command to run them and note any prerequisites (Docker for Testcontainers).

## Anti-patterns to reject

- Mocking `DbContext`/`DbSet` or testing EF queries against the InMemory provider.
- Tests that assert only "no exception" or verify every mock call.
- `Thread.Sleep`, `DateTime.Now`, real external services, and shared databases without reset.
- One giant `[Fact]` covering five behaviours; tests coupled to private methods via reflection.
- Disabling authorisation in integration tests instead of issuing test identities.
- Coverage targets chased with assertion-free tests.
- Skipped/quarantined tests with no owner or issue link.
