mcpField
T
testing

testing

github.com/codewithmukeshテスト・品質7913.0k 回インストール12 週間前に更新
リポジトリ

testing について

.NET 10でAPI、業務ロジック、データベース動作をテストするためのスキルです。実データベースを使う統合テスト、AAA構成、観測可能な動作の検証を重視します。

主な機能

  1. 01xUnit v3のテストパターン
  2. 02WebApplicationFactory統合テスト
  3. 03Testcontainersによる実DBテスト
  4. 04Verifyスナップショットテスト
  5. 05テストデータビルダー
  6. 06FakeTimeProviderによる時間依存コードのテスト

活用シーン

  1. 01APIのHTTPパイプライン全体を検証
  2. 02PostgreSQLやSQL ServerでDB動作をテスト
  3. 03複雑な応答をスナップショットで比較
  4. 04時間に依存するロジックをテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill testing

インストール後、Claude Code で/testingを入力して読み込まれたか確認します。

詳細

テスト(.NET 10)

中核となる原則

  1. 最も価値の高いテストは統合テストです — 1つの WebApplicationFactory テストで、ルーティング、バインド、検証、ビジネスロジック、永続化をまとめてカバーできます。ユニットテストを書く前に、まずここから始めてください。
  2. テストでは実際のデータベースを使用します — Testcontainers を使って、実際の PostgreSQL/SQL Server インスタンスを起動します。インメモリプロバイダーでは、実際のバグ(トランザクション、制約、SQL 生成)が見えなくなります。
  3. AAA パターンは必須です — すべてのテストを、Arrange、Act、Assert の3つの明確に分けたセクションで構成します。混在させないでください。
  4. 実装ではなく動作をテストします — リファクタリング後も通るテストにします。システムがどのように動作するかではなく、何をするかをテストしてください。

パターン

xUnit v3 の基本
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);
    }
}
WebApplicationFactory を使った統合テスト

最も価値の高いテストパターンです。HTTP パイプライン全体をテストします。

// 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 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 によるスナップショットテスト

手動でのアサーションが壊れやすい複雑なレスポンスオブジェクトには、Verify を使用します。

[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);
}

初回実行時に、Verify は .verified.txt ファイルを作成します。2回目以降は、出力を比較します。出力が変わるとテストが失敗し、差分が表示されます。

テストデータビルダー
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();
時間に依存するコードのテスト

.NET 8+ に組み込まれている TimeProvider と、Microsoft.Extensions.TimeProvider.Testing の FakeTimeProvider を使用します。

[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);
}
テストの命名規則

次のパターンを使用します: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() { }

アンチパターン

統合テストでインメモリデータベースを使わない
// 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()));
実装の詳細をテストしない
// 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);
テスト間で変更可能な状態を共有しない
// 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>()...);
アサーションのないテストを書かない
// 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);
}

選択ガイド

シナリオ推奨事項
API エンドポイントのテストWebApplicationFactory を使った統合テスト
ビジネスロジックを分離してテストフェイク/スタブを使ったユニットテスト
データベースに依存するテストTestcontainers(実際の DB)
複雑なレスポンスの検証Verify によるスナップショットテスト
時間に依存するロジックFakeTimeProvider
外部 API への依存WireMock.Net または HttpMessageHandler のスタブ
パラメーター化されたテストケース[InlineData] または [MemberData] を使った [Theory]
テストデータのセットアップビルダーパターン
コストの高いフィクスチャの共有IAsyncLifetime を使った IClassFixture<T>

コメント