Org Skills

dotnet-testing

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.

Download .zip Raw Source
When agents use itUse 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".

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill dotnet-testing -a github-copilot
# or with the org installer (adds .github/skills/dotnet-testing):
npx -y github:AGCO-Global/org-skills add skill dotnet-testing

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill dotnet-testing
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill dotnet-testing -g

Installs the backend-dotnet-skills plugin, which bundles all Backend / .NET skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-dotnet-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill dotnet-testing -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill dotnet-testing -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-testing -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-testing -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

.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

[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

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.