Skip to main content
skill CX Codex

testing · codewithmukesh — Claude Code skill for .NET 10 tests

A skill that lays out a testing strategy for .NET 10 apps: integration tests via WebApplicationFactory, real databases in Testcontainers, Verify snapshots.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-code
Open on GitHub

What testing does

The skill loads when you write tests, set up test infrastructure, or review coverage. Inside are patterns for .NET 10: plain xUnit v3 tests, integration tests through WebApplicationFactory<Program>, Testcontainers for PostgreSQL, SQL Server and Redis, Verify snapshot tests, test data builders, and FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing.

Its core claim is that one WebApplicationFactory test beats a unit test, because it exercises routing, binding, validation, business logic and persistence together. The samples show how to swap the real database for a container and run migrations inside InitializeAsync. AAA (Arrange, Act, Assert) is called mandatory, and tests should cover behavior rather than implementation.

One caveat: the samples target xUnit v3, where IAsyncLifetime.InitializeAsync returns ValueTask instead of the Task used in v2. On a v2 project you will rewrite those signatures. Testcontainers also needs Docker running, otherwise the database containers never start.

What it helps with

  • write an integration test across the whole HTTP pipeline
  • spin up a real PostgreSQL or SQL Server inside a test
  • compare an API response against a snapshot instead of hand-written asserts
  • build test data with a builder and stop duplicating setup
  • test code that depends on the current time

Example prompts for the agent

  • write an integration test for POST /api/orders using WebApplicationFactory
  • set up Testcontainers with PostgreSQL for the test suite
  • add a Verify snapshot test for the GetOrder response

How to install testing

1Skills CLI (Vercel)

npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-code

2Manually: put the files in place

~/.claude/skills/testing/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/testing/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about testing

How do I install testing in Claude?

Run npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-code. The skill file then lands at ~/.claude/skills/testing/SKILL.md.

What does the testing skill do?

It describes a testing strategy for .NET 10: xUnit v3, integration tests with WebApplicationFactory, Testcontainers for real databases, Verify snapshots and the AAA pattern. The agent picks it up whenever tests or coverage come up.

What does the skill need to run?

The examples rely on xUnit v3, Testcontainers and Verify, plus FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing for time-dependent code. Testcontainers requires Docker.

Source text

SKILL.md · MIT
---
name: testing
description: >
  Testing strategy for .NET 10 applications. Covers xUnit v3, WebApplicationFactory
  for integration tests, Testcontainers for real database testing, Verify for
  snapshot testing, and the AAA pattern.
  Load this skill when writing tests, setting up test infrastructure, reviewing
  test coverage, or when the user mentions "test", "xUnit", "WebApplicationFactory",
  "Testcontainers", "integration test", "unit test", "bUnit", "snapshot test",
  "Verify", "test coverage", "AAA pattern", "WireMock", or "FakeTimeProvider".
---

# Testing (.NET 10)

## Core Principles

1. **Integration tests are the highest-value tests** — A single `WebApplicationFactory` test covers routing, binding, validation, business logic, and persistence in one shot. Start here before writing unit tests.
2. **Real databases in tests** — Use Testcontainers to spin up real PostgreSQL/SQL Server instances. In-memory providers hide real bugs (transactions, constraints, SQL generation).
3. **AAA pattern is mandatory** — Every test has three clearly separated sections: Arrange, Act, Assert. No mixing.
4. **Test behavior, not implementation** — Tests should survive refactoring. Test what the system does, not how it does it.

## Patterns

### xUnit v3 Basics

```csharp
public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrder_WithValidItems_ReturnsSuccessResult()
    {
        // Arrange
        var db = CreateInMemoryDb();
        var clock = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero));
        var service = new OrderService(db, clock);
        var request = new CreateOrderRequest("customer-1", [new("product-1", 2)]);

        // Act
        var result = await service.CreateAsync(request);

        // Assert
        Assert.True(result.IsSuccess);
        Assert.NotEqual(Guid.Empty, result.Value.Id);
        Assert.Equal(clock.GetUtcNow(), result.Value.CreatedAt);
    }

    [Theory]
    [InlineData("")]
    [InlineData(null)]
    public async Task CreateOrder_WithInvalidCustomerId_ReturnsFailure(string? customerId)
    {
        // Arrange
        var service = CreateService();

        // Act
        var result = await service.CreateAsync(new CreateOrderRequest(customerId!, []));

        // Assert
        Assert.False(result.IsSuccess);
    }
}
```

### Integration Tests with WebApplicationFactory

The highest-value test pattern. Tests the full HTTP pipeline.

```csharp
// Fixtures/ApiFixture.cs
public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:18")
        .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Replace the real DB with Testcontainers
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(options =>
                options.UseNpgsql(_postgres.GetConnectionString()));
        });
    }

    // xUnit v3: IAsyncLifetime.InitializeAsync returns ValueTask (v2 used Task)
    public async ValueTask InitializeAsync()
    {
        await _postgres.StartAsync();

        // Apply migrations
        using var scope = Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await db.Database.MigrateAsync();
    }

    // xUnit v3: IAsyncLifetime inherits IAsyncDisposable — override the
    // ValueTask DisposeAsync that WebApplicationFactory already provides
    public override async ValueTask DisposeAsync()
    {
        await _postgres.DisposeAsync();
        await base.DisposeAsync();
    }
}
```

```csharp
// Tests/Orders/CreateOrderTests.cs
public class CreateOrderTests(ApiFixture fixture) : IClassFixture<ApiFixture>
{
    private readonly HttpClient _client = fixture.CreateClient();

    [Fact]
    public async Task CreateOrder_ReturnsCreated_WithValidRequest()
    {
        // Arrange
        var request = new CreateOrderRequest("customer-1", [new("product-1", 2)]);

        // Act
        var response = await _client.PostAsJsonAsync("/api/orders", request);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);

        var order = await response.Content.ReadFromJsonAsync<OrderResponse>();
        Assert.NotNull(order);
        Assert.NotEqual(Guid.Empty, order.Id);
        Assert.Contains("/api/orders/", response.Headers.Location?.ToString());
    }

    [Fact]
    public async Task CreateOrder_ReturnsValidationProblem_WithEmptyItems()
    {
        // Arrange
        var request = new CreateOrderRequest("customer-1", []);

        // Act
        var response = await _client.PostAsJsonAsync("/api/orders", request);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}
```

### Testcontainers for Real Database Testing

```csharp
// For SQL Server
private readonly MsSqlContainer _mssql = new MsSqlBuilder()
    .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
    .Build();

// For PostgreSQL
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
    .WithImage("postgres:18")
    .Build();

// For Redis
private readonly RedisContainer _redis = new RedisBuilder()
    .WithImage("redis:7")
    .Build();
```

### Verify Snapshot Testing

Use Verify for complex response objects where manual assertions would be fragile.

```csharp
[Fact]
public async Task GetOrder_MatchesSnapshot()
{
    // Arrange
    await SeedOrder(fixture);

    // Act
    var response = await _client.GetAsync("/api/orders/known-id");
    var content = await response.Content.ReadAsStringAsync();

    // Assert — compares against a stored .verified.txt file
    await Verify(content);
}
```

On first run, Verify creates a `.verified.txt` file. On subsequent runs, it compares output. If the output changes, the test fails and shows a diff.

### Test Data Builders

```csharp
public class OrderBuilder
{
    private string _customerId = "default-customer";
    private List<OrderItem> _items = [new("product-1", 1, 9.99m)];
    private OrderStatus _status = OrderStatus.Pending;

    public OrderBuilder WithCustomer(string customerId)
    {
        _customerId = customerId;
        return this;
    }

    public OrderBuilder WithItems(params OrderItem[] items)
    {
        _items = [..items];
        return this;
    }

    public OrderBuilder WithStatus(OrderStatus status)
    {
        _status = status;
        return this;
    }

    public Order Build() => Order.Create(_customerId, _items, _status);
}

// Usage in tests
var order = new OrderBuilder()
    .WithCustomer("vip-customer")
    .WithStatus(OrderStatus.Confirmed)
    .Build();
```

### Testing Time-Dependent Code

Use `TimeProvider` (built into .NET 8+) and `FakeTimeProvider` from `Microsoft.Extensions.TimeProvider.Testing`.

```csharp
[Fact]
public async Task ExpireOrders_MarksOldPendingOrdersAsExpired()
{
    // Arrange
    var clock = new FakeTimeProvider(new DateTimeOffset(2025, 6, 1, 0, 0, 0, TimeSpan.Zero));
    var db = CreateDb();
    var order = Order.Create("customer-1", items, clock.GetUtcNow());
    db.Orders.Add(order);
    await db.SaveChangesAsync();

    // Advance time past expiry threshold
    clock.Advance(TimeSpan.FromDays(31));

    var handler = new ExpireOrders.Handler(db, clock);

    // Act
    await handler.Handle(new ExpireOrders.Command(), CancellationToken.None);

    // Assert
    var updated = await db.Orders.FindAsync(order.Id);
    Assert.Equal(OrderStatus.Expired, updated!.Status);
}
```

### Test Naming Convention

Use the pattern: `MethodName_StateUnderTest_ExpectedBehavior`

```csharp
[Fact] public async Task CreateOrder_WithValidItems_ReturnsSuccessResult() { }
[Fact] public async Task CreateOrder_WithEmptyItems_ReturnsValidationError() { }
[Fact] public async Task GetOrder_WithNonExistentId_ReturnsNotFound() { }
[Fact] public async Task CancelOrder_WhenAlreadyShipped_ReturnsConflict() { }
```

## Anti-patterns

### Don't Use In-Memory Database for Integration Tests

```csharp
// BAD — hides real SQL behavior, transactions, constraints
services.AddDbContext<AppDbContext>(options =>
    options.UseInMemoryDatabase("TestDb"));

// GOOD — Testcontainers with real database
services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(testContainer.GetConnectionString()));
```

### Don't Test Implementation Details

```csharp
// BAD — testing that a specific repository method was called
mock.Verify(x => x.AddAsync(It.IsAny<Order>()), Times.Once);
mock.Verify(x => x.SaveChangesAsync(), Times.Once);

// GOOD — test the observable outcome
var order = await db.Orders.FindAsync(orderId);
Assert.NotNull(order);
Assert.Equal(OrderStatus.Created, order.Status);
```

### Don't Share Mutable State Between Tests

```csharp
// BAD — static shared state
private static readonly AppDbContext SharedDb = CreateDb();

// GOOD — fresh state per test (or use IAsyncLifetime for shared fixtures)
private AppDbContext CreateDb() => new(new DbContextOptionsBuilder<AppDbContext>()...);
```

### Don't Write Assertion-Free Tests

```csharp
// BAD — no assertion, only checks it doesn't throw
[Fact]
public async Task CreateOrder_Works()
{
    await service.CreateAsync(request);
    // "it didn't throw, so it works!" — NO
}

// GOOD — assert the expected outcome
[Fact]
public async Task CreateOrder_PersistsOrderToDatabase()
{
    var result = await service.CreateAsync(request);

    var persisted = await db.Orders.FindAsync(result.Value.Id);
    Assert.NotNull(persisted);
    Assert.Equal(request.CustomerId, persisted.CustomerId);
}
```

## Decision Guide

| Scenario | Recommendation |
|----------|---------------|
| Testing an API endpoint | `WebApplicationFactory` integration test |
| Testing business logic in isolation | Unit test with fakes/stubs |
| Database-dependent tests | Testcontainers (real DB) |
| Complex response validation | Verify snapshot testing |
| Time-dependent logic | `FakeTimeProvider` |
| External API dependency | `WireMock.Net` or `HttpMessageHandler` stub |
| Parameterized test cases | `[Theory]` with `[InlineData]` or `[MemberData]` |
| Test data setup | Builder pattern |
| Shared expensive fixture | `IClassFixture<T>` with `IAsyncLifetime` |

© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit

Similar skills

Everything for Testing and QA →

testing by other authors comes first.

testing tursodatabase Skill teaches you to write tests for SQL and Rust: choose between .sqltest, TCL and integration formats, run test suites with the right commands. CX Codex +1 1,300installs testing grafana Sets up uptime probes, k6 load tests, and frontend observability through Grafana Cloud. Use it before a release or when investigating failures. CX Codex +1 4,200installs testing rshankras A set of TDD workflows and test infrastructure for Apple platform apps. Useful when an agent writes the code and you want tests around it. CX Codex +1 468installs testing modelcontextprotocol The skill covers where tests live for each MCP server, which runner picks them up, and how to clear the per-file coverage gate. Useful when a test is skipped or passes against a stale build. CX Codex +1 ★ 91,547repo stars snapshot-testing Aaronontheweb A skill that gets your agent writing Verify snapshot tests: output is compared against a baseline you approve by hand. CX Codex +1 461installs testcontainers wshaddix A skill for .NET integration tests: spins up real databases, Redis and message brokers in Docker instead of mocks. CX Codex +1 565installs aspire-integration-testing Aaronontheweb A skill for writing .NET Aspire integration tests with xUnit: fixtures, distributed application startup, endpoint discovery. CX Codex +1 499installs csharp-testing affaan-m A skill that gives your agent C# and .NET testing patterns: xUnit, FluentAssertions, mocks, integration tests. CX Codex +1 4,500installs 322-frameworks-spring-boot-testing-integration-tests Juan Antonio Breña Moral A skill for writing and reviewing Spring Boot 4.0.x integration tests: Testcontainers, persistence slices, HTTP assertions. CX Codex +1 357installs planning-oracle-to-postgres-migration-integration-testing official github This skill inspects a single .NET project and writes an integration test plan for an Oracle-to-PostgreSQL migration. Use it to map database touchpoints to tests. 1,800installs

From the same set46

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor