Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" 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(){// Arrangevar 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)]);// Actvar result = await service.CreateAsync(request);// AssertAssert.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){// Arrangevar service = CreateService();// Actvar result = await service.CreateAsync(new CreateOrderRequest(customerId!, []));// AssertAssert.False(result.IsSuccess);}}
Integration Tests with WebApplicationFactory
The highest-value test pattern. Tests the full HTTP pipeline.
// Fixtures/ApiFixture.cspublic 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 Testcontainersservices.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 migrationsusing 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 providespublic override async ValueTask DisposeAsync(){await _postgres.DisposeAsync();await base.DisposeAsync();}}
// Tests/Orders/CreateOrderTests.cspublic class CreateOrderTests(ApiFixture fixture) : IClassFixture<ApiFixture>{private readonly HttpClient _client = fixture.CreateClient();[Fact]public async Task CreateOrder_ReturnsCreated_WithValidRequest(){// Arrangevar request = new CreateOrderRequest("customer-1", [new("product-1", 2)]);// Actvar response = await _client.PostAsJsonAsync("/api/orders", request);// AssertAssert.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(){// Arrangevar request = new CreateOrderRequest("customer-1", []);// Actvar response = await _client.PostAsJsonAsync("/api/orders", request);// AssertAssert.Equal(HttpStatusCode.BadRequest, response.StatusCode);}}
Testcontainers for Real Database Testing
// For SQL Serverprivate readonly MsSqlContainer _mssql = new MsSqlBuilder().WithImage("mcr.microsoft.com/mssql/server:2022-latest").Build();// For PostgreSQLprivate readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().WithImage("postgres:18").Build();// For Redisprivate 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(){// Arrangeawait SeedOrder(fixture);// Actvar response = await _client.GetAsync("/api/orders/known-id");var content = await response.Content.ReadAsStringAsync();// Assert — compares against a stored .verified.txt fileawait 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 testsvar 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(){// Arrangevar 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 thresholdclock.Advance(TimeSpan.FromDays(31));var handler = new ExpireOrders.Handler(db, clock);// Actawait handler.Handle(new ExpireOrders.Command(), CancellationToken.None);// Assertvar 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, constraintsservices.AddDbContext<AppDbContext>(options =>options.UseInMemoryDatabase("TestDb"));// GOOD — Testcontainers with real databaseservices.AddDbContext<AppDbContext>(options =>options.UseNpgsql(testContainer.GetConnectionString()));
Don't Test Implementation Details
// BAD — testing that a specific repository method was calledmock.Verify(x => x.AddAsync(It.IsAny<Order>()), Times.Once);mock.Verify(x => x.SaveChangesAsync(), Times.Once);// GOOD — test the observable outcomevar 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 stateprivate 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 |