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

在 session 結束時把已完成工作、待辦事項與經驗寫入交接檔案,並在 session 開始時重新載入。

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 個月前更新