testing
codewithmukesh/dotnet-claude-kit
xUnit v3, WebApplicationFactory, and Testcontainers testing patterns for .NET 10 applications.
What is testing?
Testing strategy for .NET 10 applications covering unit tests with xUnit v3, integration tests with WebApplicationFactory, real database testing with Testcontainers, snapshot testing with Verify, and the AAA pattern. Load this skill when writing tests, setting up test infrastructure, or reviewing test coverage.
- Write unit tests with xUnit v3 using [Fact] and [Theory] attributes
- Build integration tests with WebApplicationFactory that test the full HTTP pipeline
- Spin up real PostgreSQL, SQL Server, and Redis instances in tests using Testcontainers
- Create snapshot tests with Verify for complex response objects
- Test time-dependent code using FakeTimeProvider and TimeProvider
- Build test data fluently with builder patterns and follow AAA (Arrange-Act-Assert) structure
How to install testing
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill testing- xUnit v3 NuGet package
- Testcontainers NuGet package (for real database testing)
- Verify NuGet package (for snapshot testing)
- Microsoft.Extensions.TimeProvider.Testing (for FakeTimeProvider)
- Docker running locally (for Testcontainers)
How to use testing
- 1.Create a test fixture class inheriting from WebApplicationFactory<Program> and IAsyncLifetime
- 2.Configure Testcontainers (PostgreSQL, SQL Server, Redis) in the fixture's ConfigureWebHost method
- 3.Implement InitializeAsync to start containers and apply migrations
- 4.Write integration tests using [Fact] with the AAA pattern: Arrange test data, Act by calling the API, Assert on responses
- 5.Use FakeTimeProvider in Arrange sections to control time for time-dependent tests
- 6.Create builder classes for complex test data to keep tests readable and maintainable
Use cases
- Writing integration tests that cover routing, binding, validation, business logic, and persistence in a single test
- Testing database interactions with real containers instead of in-memory providers to catch transaction and constraint bugs
- Creating snapshot tests for API responses that need to survive refactoring
- Testing expiration logic and time-dependent behavior with controlled time advancement
- Validating input validation and error handling across the HTTP pipeline
- Backend developers building .NET 10 applications
- QA engineers setting up test infrastructure
- Teams adopting integration-test-first strategies
- Developers reviewing or improving test coverage
testing FAQ
A single WebApplicationFactory test covers routing, binding, validation, business logic, and persistence in one shot. They provide higher value and are less brittle than unit tests that mock dependencies.
No. In-memory providers hide real bugs related to transactions, constraints, and SQL generation. Use Testcontainers to spin up real PostgreSQL or SQL Server instances instead.
Arrange (set up test data and dependencies), Act (execute the code being tested), Assert (verify the results). Each section must be clearly separated with no mixing of concerns.
Use FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing. Create it with a specific DateTimeOffset, inject it into your service, and call clock.Advance() to move time forward in tests.
Use Verify for complex response objects where manual assertions would be fragile. It compares output against stored .verified.txt files and shows diffs when output changes.
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
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
- Integration tests are the highest-value tests — A single
WebApplicationFactorytest covers routing, binding, validation, business logic, and persistence in one shot. Start here before writing unit tests. - Real databases in tests — Use Testcontainers to spin up real PostgreSQL/SQL Server instances. In-memory providers hide real bugs (transactions, constraints, SQL generation).
- AAA pattern is mandatory — Every test has three clearly separated sections: Arrange, Act, Assert. No mixing.
- Test behavior, not implementation — Tests should survive refactoring. Test what the system does, not how it does it.
Patterns
xUnit v3 Basics
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.
// 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();
}
}
// 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
// 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.
[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
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.
[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
[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
// 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
// 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
// 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
// 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 |
Related skills
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

verify
7-phase verification pipeline for .NET projects: build, diagnostics, antipatterns, tests, security, formatting, and diff review.

vertical-slice
Vertical Slice Architecture for .NET — organize features as self-contained slices, not layers.

workflow-mastery
Master Claude Code workflows for .NET: parallel sessions, plan mode, verification loops, and context optimization.

wrap-up
Capture session work, pending tasks, and learnings into a handoff file for continuity across sessions.

mintlify
Comprehensive reference for building and configuring Mintlify documentation sites.

codex-theme-installer
Download and install published Codex themes from codexthemes.ai into your local theme library.