Testing

codewithmukesh/dotnet-claude-kit/skills/testing

作者 codewithmukesh23300897f4d1无许可证754 个星标收录于 2026年10月8日更新于 2026年10月8日仓库2个月前更新

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.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 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.cspublic 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 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.

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 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.

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, constraintsservices.AddDbContext<AppDbContext>(options =>    options.UseInMemoryDatabase("TestDb"));
// GOOD — Testcontainers with real databaseservices.AddDbContext<AppDbContext>(options =>    options.UseNpgsql(testContainer.GetConnectionString()));

Don't Test Implementation Details

csharp
// 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

csharp
// 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

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

ScenarioRecommendation
Testing an API endpointWebApplicationFactory integration test
Testing business logic in isolationUnit test with fakes/stubs
Database-dependent testsTestcontainers (real DB)
Complex response validationVerify snapshot testing
Time-dependent logicFakeTimeProvider
External API dependencyWireMock.Net or HttpMessageHandler stub
Parameterized test cases[Theory] with [InlineData] or [MemberData]
Test data setupBuilder pattern
Shared expensive fixtureIClassFixture<T> with IAsyncLifetime

来源与署名

来源:codewithmukesh/dotnet-claude-kit位于skills/testing提交2330089

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架

更多来自 codewithmukesh/dotnet-claude-kit 的技能

Wrap Up

codewithmukesh

在会话结束时把已完成工作、待办任务与经验写入交接文件,并在会话开始时重新载入。

Productivity & Workflow7542个月前更新

Workflow Mastery

codewithmukesh

Claude Code workflow mastery for .NET developers. Covers parallel execution with git worktrees, plan mode strategy, verification loops, auto-formatting hooks, permission setup for dotnet CLI, prompting techniques, subagent patterns, and context discipline — token budget management, MCP-first navigation, lazy loading, and subagent isolation — all adapted for the .NET ecosystem. Load this skill when setting up Claude Code for a .NET project, optimizing workflows, running parallel sessions, when context is running low or sessions feel sluggish, when exploring a large codebase efficiently, or when the user mentions "productivity", "workflow", "parallel", "worktree", "plan mode", "permissions", "hooks", "10x", "setup Claude Code", "speed up development", "context", "tokens", "budget", "running out of context", "too many files", or "large codebase". Inspired by tips from Boris Cherny (creator of Claude Code) and the Anthropic team.

待分类7542个月前更新

Vertical Slice

codewithmukesh

指导 .NET 开发者采用垂直切片架构组织应用,涵盖功能文件夹、端点分组与处理程序模式。

Software Development7542个月前更新

Tdd

codewithmukesh

Guided test-driven development workflow for .NET 10 using xUnit v3, WebApplicationFactory, Testcontainers, and Verify snapshots. Follows the strict red-green-refactor cycle. Use when: "TDD", "test-driven", "let's TDD this", "red green refactor", "write the test first", or when building a feature with clear acceptance criteria.

待分类7542个月前更新

Spec

codewithmukesh

通过结构化提问,把模糊的功能想法转化为双方确认并持久化的规格说明文件。

Productivity & Workflow7542个月前更新

Serilog

codewithmukesh

Structured logging with Serilog for .NET 10 applications. Covers two-stage bootstrap, appsettings configuration, enrichers, sinks, request logging, destructuring, and Serilog.Expressions. Load this skill when setting up Serilog, configuring log sinks, enrichers, or structured logging, or when the user mentions "Serilog", "structured logging", "log enrichment", "Seq", "LogContext", "UseSerilog", "WriteTo", "message template", "Serilog.Expressions", "request logging", "log sink", "rolling file", or "audit log".

待分类7542个月前更新