New to Claude Skills? Learn how to install them →

affaan-m on GitHub

C# Testing Patterns

Free

Comprehensive testing patterns for .NET applications.

by affaan-m239.3k stars on affaan-m/ecc
2 views
Updated Aug 10, 2026
Get this skill

Free · Opens the source repo

What C# Testing Patterns does

C# Testing Patterns provides a structured approach to writing tests for .NET applications using established frameworks and best practices. It leverages xUnit for unit testing, FluentAssertions for expressive assertions, and mocking libraries like NSubstitute or Moq to handle dependencies. This skill is particularly valuable for developers looking to enhance the quality and coverage of their tests while adhering to modern testing methodologies.

The skill is designed to assist in various scenarios, such as writing new tests, reviewing existing test quality, setting up testing infrastructure for .NET projects, and debugging flaky or slow tests. It integrates seamlessly with tools like Testcontainers for integration testing and WebApplicationFactory for ASP.NET Core applications, ensuring that your tests are both reliable and maintainable.

By following the Arrange-Act-Assert pattern, users can organize their tests clearly and concisely. The skill also supports parameterized tests using Theory, allowing for comprehensive testing of edge cases. With practical examples and a focus on real-world applications, developers can quickly adapt these patterns to their projects, improving test reliability and reducing the time spent on debugging.

Whether you are a seasoned developer or new to C#, this skill provides the necessary tools and patterns to implement effective testing strategies. It is an essential resource for anyone involved in .NET development who aims to produce high-quality, maintainable code.

When to use it

Use this skill when you need to write new tests, improve existing test coverage, or debug issues in your .NET testing setup.

When not to use it

This skill may not be suitable for projects that do not use .NET or for teams looking for non-standard testing practices.

What you can build with it

Writing New Tests

Use this skill to guide the creation of new unit tests for your C# applications, ensuring they follow best practices.

Reviewing Test Quality

Leverage the skill to evaluate existing tests for quality and coverage, helping to identify areas for improvement.

Setting Up Test Infrastructure

Utilize the skill to establish a robust testing infrastructure for your .NET projects, including integration testing setups.

How to install C# Testing Patterns

View source

1. Install with the skills CLI

npx skills add affaan-m/ecc/csharp-testing --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by affaan-m

C#テストパターン

xUnit、FluentAssertions、最新のテストプラクティスを使用した.NETアプリケーションの包括的なテストパターン。

起動条件

  • C#コードの新しいテストを書く場合
  • テスト品質とカバレッジのレビュー
  • .NETプロジェクトのテストインフラストラクチャの設定
  • フレーキーまたは遅いテストのデバッグ

テストフレームワークスタック

ツール目的
xUnitテストフレームワーク(.NETに推奨)
FluentAssertions読みやすいアサーション構文
NSubstituteまたはMoq依存関係のモッキング
Testcontainers統合テストでの実際のインフラ
WebApplicationFactoryASP.NET Core統合テスト
Bogus現実的なテストデータ生成

ユニットテスト構造

Arrange-Act-Assert

public sealed class OrderServiceTests
{
    private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
    private readonly ILogger<OrderService> _logger = Substitute.For<ILogger<OrderService>>();
    private readonly OrderService _sut;

    public OrderServiceTests()
    {
        _sut = new OrderService(_repository, _logger);
    }

    [Fact]
    public async Task PlaceOrderAsync_ReturnsSuccess_WhenRequestIsValid()
    {
        // Arrange
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-123",
            Items = [new OrderItem("SKU-001", 2, 29.99m)]
        };

        // Act
        var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);

        // Assert
        result.IsSuccess.Should().BeTrue();
        result.Value.Should().NotBeNull();
        result.Value!.CustomerId.Should().Be("cust-123");
    }

    [Fact]
    public async Task PlaceOrderAsync_ReturnsFailure_WhenNoItems()
    {
        // Arrange
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-123",
            Items = []
        };

        // Act
        var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);

        // Assert
        result.IsSuccess.Should().BeFalse();
        result.Error.Should().Contain("at least one item");
    }
}

Theoryによるパラメータ化テスト

[Theory]
[InlineData("", false)]
[InlineData("a", false)]
[InlineData("ab@c.d", false)]
[InlineData("user@example.com", true)]
[InlineData("user+tag@example.co.uk", true)]
public void IsValidEmail_ReturnsExpected(string email, bool expected)
{
    EmailValidator.IsValid(email).Should().Be(expected);
}

[Theory]
[MemberData(nameof(InvalidOrderCases))]
public async Task PlaceOrderAsync_RejectsInvalidOrders(CreateOrderRequest request, string expectedError)
{
    var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);

    result.IsSuccess.Should().BeFalse();
    result.Error.Should().Contain(expectedError);
}

public static TheoryData<CreateOrderRequest, string> InvalidOrderCases => new()
{
    { new() { CustomerId = "", Items = [ValidItem()] }, "CustomerId" },
    { new() { CustomerId = "c1", Items = [] }, "at least one item" },
    { new() { CustomerId = "c1", Items = [new("", 1, 10m)] }, "SKU" },
};

NSubstituteによるモッキング

[Fact]
public async Task GetOrderAsync_ReturnsNull_WhenNotFound()
{
    // Arrange
    var orderId = Guid.NewGuid();
    _repository.FindByIdAsync(orderId, Arg.Any<CancellationToken>())
        .Returns((Order?)null);

    // Act
    var result = await _sut.GetOrderAsync(orderId, CancellationToken.None);

    // Assert
    result.Should().BeNull();
}

[Fact]
public async Task PlaceOrderAsync_PersistsOrder()
{
    // Arrange
    var request = ValidOrderRequest();

    // Act
    await _sut.PlaceOrderAsync(request, CancellationToken.None);

    // Assert — リポジトリが呼び出されたことを検証
    await _repository.Received(1).AddAsync(
        Arg.Is<Order>(o => o.CustomerId == request.CustomerId),
        Arg.Any<CancellationToken>());
}

ASP.NET Core統合テスト

WebApplicationFactoryのセットアップ

public sealed class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrderApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // テスト用にインメモリDBで実際のDBを置き換え
                services.RemoveAll<DbContextOptions<AppDbContext>>();
                services.AddDbContext<AppDbContext>(options =>
                    options.UseInMemoryDatabase("TestDb"));
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetOrder_Returns404_WhenNotFound()
    {
        var response = await _client.GetAsync($"/api/orders/{Guid.NewGuid()}");

        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task CreateOrder_Returns201_WithValidRequest()
    {
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-1",
            Items = [new("SKU-001", 1, 19.99m)]
        };

        var response = await _client.PostAsJsonAsync("/api/orders", request);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
    }
}

Testcontainersによるテスト

public sealed class PostgresOrderRepositoryTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .Build();

    private AppDbContext _db = null!;

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_postgres.GetConnectionString())
            .Options;
        _db = new AppDbContext(options);
        await _db.Database.MigrateAsync();
    }

    public async Task DisposeAsync()
    {
        await _db.DisposeAsync();
        await _postgres.DisposeAsync();
    }

    [Fact]
    public async Task AddAsync_PersistsOrder()
    {
        var repo = new SqlOrderRepository(_db);
        var order = Order.Create("cust-1", [new OrderItem("SKU-001", 2, 10m)]);

        await repo.AddAsync(order, CancellationToken.None);

        var found = await repo.FindByIdAsync(order.Id, CancellationToken.None);
        found.Should().NotBeNull();
        found!.Items.Should().HaveCount(1);
    }
}

テスト組織

tests/
  MyApp.UnitTests/
    Services/
      OrderServiceTests.cs
      PaymentServiceTests.cs
    Validators/
      EmailValidatorTests.cs
  MyApp.IntegrationTests/
    Api/
      OrderApiTests.cs
    Repositories/
      OrderRepositoryTests.cs
  MyApp.TestHelpers/
    Builders/
      OrderBuilder.cs
    Fixtures/
      DatabaseFixture.cs

テストデータビルダー

public sealed class OrderBuilder
{
    private string _customerId = "cust-default";
    private readonly List<OrderItem> _items = [new("SKU-001", 1, 10m)];

    public OrderBuilder WithCustomer(string customerId)
    {
        _customerId = customerId;
        return this;
    }

    public OrderBuilder WithItem(string sku, int quantity, decimal price)
    {
        _items.Add(new OrderItem(sku, quantity, price));
        return this;
    }

    public Order Build() => Order.Create(_customerId, _items);
}

// テストでの使用
var order = new OrderBuilder()
    .WithCustomer("cust-vip")
    .WithItem("SKU-PREMIUM", 3, 99.99m)
    .Build();

よくあるアンチパターン

アンチパターン修正方法
実装の詳細をテストする動作と結果をテストする
共有の可変テスト状態テストごとに新しいインスタンス(xUnitはコンストラクタでこれを行う)
非同期テストでのThread.Sleepタイムアウトまたはポーリングヘルパーを使用したTask.Delay
ToString()出力のアサーション型付きプロパティのアサーション
テストごとに1つの巨大なアサーションテストごとに1つの論理的なアサーション
実装を記述するテスト名動作で命名: Method_ExpectedResult_WhenCondition
CancellationTokenを無視する常に渡してキャンセルを確認する

テストの実行

# すべてのテストを実行
dotnet test

# カバレッジを付けて実行
dotnet test --collect:"XPlat Code Coverage"

# 特定のプロジェクトを実行
dotnet test tests/MyApp.UnitTests/

# テスト名でフィルタリング
dotnet test --filter "FullyQualifiedName~OrderService"

# 開発中のウォッチモード
dotnet watch test --project tests/MyApp.UnitTests/

Frequently asked questions about C# Testing Patterns

Similar skills