Communitygithub.com

C# 테스트 패턴

C# 및 .NET 애플리케이션에서 xUnit, FluentAssertions, 목킹(Mocking), 통합 테스트 및 테스트 구성 모범 사례를 다루는 종합적인 테스트 패턴입니다.

C# 테스트 패턴란 무엇인가요?

이 스킬은 .NET 개발자가 xUnit을 주 테스트 프레임워크로 사용하고 FluentAssertions로 가독성 높은 어설션을 작성하며, NSubstitute 또는 Moq로 의존성을 목킹하고, Testcontainers로 실제 인프라를 활용한 통합 테스트, WebApplicationFactory로 ASP.NET Core 통합 테스트를 수행하는 방법을 제공합니다. Arrange-Act-Assert 구조, 이론(Theory) 기반 매개변수화 테스트, 테스트 데이터 빌더 패턴, 그리고 테스트 조직(UnitTests/IntegrationTests/TestHelpers)에 대한 명확한 지침을 포함합니다. 또한 `Thread.Sleep` 사용, 구현 세부사항 테스트, 공유 가변 상태 등 일반적인 안티패턴을 식별하고 해결 방법을 제시합니다. CLI 명령어(`dotnet test`, 커버리지 수집, 필터링, 감시 모드)도 포함되어 있어 실무에서 바로 활용할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/csharp-testing

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

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()
    {
        // 준비
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-123",
            Items = [new OrderItem("SKU-001", 2, 29.99m)]
        };

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

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

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

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

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

Theory를 사용한 매개변수화 테스트

[Theory]
[InlineData("", false)]
[InlineData("a", false)]
[InlineData("[email protected]", false)]
[InlineData("[email protected]", true)]
[InlineData("[email protected]", 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()
{
    // 준비
    var orderId = Guid.NewGuid();
    _repository.FindByIdAsync(orderId, Arg.Any<CancellationToken>())
        .Returns((Order?)null);

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

    // 검증
    result.Should().BeNull();
}

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

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

    // 검증 - 리포지토리가 호출되었는지 확인
    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은 생성자에서 처리)
async 테스트에서 Thread.Sleep 사용Task.Delay와 타임아웃 또는 폴링 헬퍼 사용
ToString() 출력으로 어설션타입 속성에 대해 어설션
테스트당 하나의 거대한 어설션테스트당 하나의 논리적 어설션
구현을 설명하는 테스트 이름동작으로 이름 지정: 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/

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬