Community코딩 & 개발github.com

testcontainers-integration-tests

TestContainers를 활용하여 .NET 및 xUnit 기반의 통합 테스트를 작성합니다. 실제 데이터베이스, 메시지 큐, 캐시 등 인프라를 Docker 컨테이너로 띄워 목(mock) 대신 사용하는 방법을 다룹니다.

testcontainers-integration-tests란 무엇인가요?

이 스킬은 .NET 개발자가 TestContainers 라이브러리를 사용해 실제 인프라(데이터베이스, 캐시, 메시지 큐)를 도커 컨테이너로 실행하는 통합 테스트를 작성하는 방법을 설명합니다. 기존의 목(mock) 기반 테스트가 놓칠 수 있는 SQL 쿼리 검증, 제약 조건, 트랜잭션 동작, 인덱스 등을 실제 환경과 유사하게 테스트하여 더 높은 신뢰성을 제공합니다. 주요 패턴으로 SQL Server, PostgreSQL, MySQL 데이터베이스 테스트와 Redis, RabbitMQ 같은 메시지 큐/캐시 테스트를 포함하며, CI/CD(GitHub Actions)에서 Docker 지원 환경에서 바로 사용할 수 있습니다. 또한 컨테이너 재사용, 무작위 포트 할당, Respawn을 통한 데이터 초기화 등 성능 최적화 방법과 함께 컨테이너 생명주기 관리(자동 정리)에 대한 모범 사례를 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/testcontainers

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

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

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

문서

TestContainers를 사용한 통합 테스트

이 스킬을 사용해야 할 때

다음과 같은 상황에서 이 스킬을 사용하세요:

  • 실제 인프라(데이터베이스, 캐시, 메시지 큐)가 필요한 통합 테스트를 작성할 때
  • 데이터 액세스 계층을 실제 데이터베이스에 대해 테스트할 때
  • 메시지 큐 통합을 검증할 때
  • Redis 캐싱 동작을 테스트할 때
  • 인프라 컴포넌트에 대한 목(mock)을 피하고 싶을 때
  • 테스트가 프로덕션과 유사한 환경에서 작동하는지 확인할 때
  • 데이터베이스 마이그레이션 및 스키마 변경을 테스트할 때

참고 파일

핵심 원칙

  1. 실제 인프라가 목보다 낫다 - 목 대신 실제 데이터베이스/서비스를 컨테이너에서 사용
  2. 테스트 격리 - 각 테스트는 신규 컨테이너나 신선한 데이터를 사용
  3. 자동 정리 - TestContainers가 컨테이너 생명주기와 정리를 관리
  4. 빠른 시작 - 같은 클래스 내 테스트 간에 컨테이너를 적절히 재사용
  5. CI/CD 호환 - Docker를 지원하는 CI 환경에서 바로 작동
  6. 포트 무작위화 - 컨테이너는 충돌을 피하기 위해 무작위 포트 사용

왜 TestContainers가 목보다 나은가?

목을 사용한 인프라 테스트의 문제점

// 나쁜 예: 데이터베이스 목(mock) 사용
public class OrderRepositoryTests
{
    private readonly Mock<IDbConnection> _mockDb = new();

    [Fact]
    public async Task GetOrder_ReturnsOrder()
    {
        // 실제 SQL 동작, 제약 조건, 성능을 테스트하지 않음
        _mockDb.Setup(db => db.QueryAsync<Order>(It.IsAny<string>()))
            .ReturnsAsync(new[] { new Order { Id = 1 } });

        var repo = new OrderRepository(_mockDb.Object);
        var order = await repo.GetOrderAsync(1);

        Assert.NotNull(order);
    }
}

문제점: 실제 SQL 쿼리를 테스트하지 않으며, 제약 조건/인덱스를 놓치고, 잘못된 확신을 주며, SQL 문법 오류를 잡지 못합니다.

더 나은 방법: 실제 데이터베이스와 함께 TestContainers 사용

// 좋은 예: 실제 데이터베이스에 대한 테스트
public class OrderRepositoryTests : IAsyncLifetime
{
    private readonly TestcontainersContainer _dbContainer;
    private IDbConnection _connection;

    public OrderRepositoryTests()
    {
        _dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithEnvironment("ACCEPT_EULA", "Y")
            .WithEnvironment("SA_PASSWORD", "Your_password123")
            .WithPortBinding(1433, true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
        var port = _dbContainer.GetMappedPublicPort(1433);
        var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";
        _connection = new SqlConnection(connectionString);
        await _connection.OpenAsync();
        await RunMigrationsAsync(_connection);
    }

    public async Task DisposeAsync()
    {
        await _connection.DisposeAsync();
        await _dbContainer.DisposeAsync();
    }

    [Fact]
    public async Task GetOrder_WithRealDatabase_ReturnsOrder()
    {
        await _connection.ExecuteAsync(
            "INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");

        var repo = new OrderRepository(_connection);
        var order = await repo.GetOrderAsync(1);

        Assert.NotNull(order);
        Assert.Equal("CUST1", order.CustomerId);
        Assert.Equal(100.00m, order.Total);
    }
}

전체 SQL Server, PostgreSQL 및 마이그레이션 테스트 예제는 database-patterns.md를 참조하세요.

Redis, RabbitMQ, 다중 컨테이너 네트워크, 컨테이너 재사용 및 Respawn 데이터베이스 리셋 패턴은 infrastructure-patterns.md를 참조하세요.

필수 NuGet 패키지

<ItemGroup>
  <PackageReference Include="Testcontainers" Version="*" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />

  <!-- 데이터베이스별 패키지 -->
  <PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
  <PackageReference Include="Npgsql" Version="*" /> <!-- PostgreSQL용 -->
  <PackageReference Include="MySqlConnector" Version="*" /> <!-- MySQL용 -->

  <!-- 기타 인프라 -->
  <PackageReference Include="StackExchange.Redis" Version="*" /> <!-- Redis용 -->
  <PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- RabbitMQ용 -->
</ItemGroup>

모범 사례

  1. 항상 IAsyncLifetime 사용 - 올바른 비동기 설정 및 정리
  2. 포트 사용 가능 대기 - WaitStrategy를 사용해 컨테이너가 준비될 때까지 대기
  3. 무작위 포트 사용 - TestContainers가 포트를 자동으로 할당하도록 함
  4. 테스트 간 데이터 정리 - 신규 컨테이너를 사용하거나 테이블을 잘라냄(truncate)
  5. 가능한 경우 컨테이너 재사용 - 각 테스트마다 새로 만드는 것보다 빠름
  6. 실제 쿼리 테스트 - 목만 테스트하지 말고 실제 SQL 동작을 검증
  7. 제약 조건 확인 - 외래 키, 고유 제약 조건, 인덱스 테스트
  8. 트랜잭션 테스트 - 롤백 및 커밋 동작 검증
  9. 현실적인 데이터 사용 - 프로덕션과 유사한 데이터 양으로 테스트
  10. 정리 처리 - DisposeAsync에서 항상 컨테이너를 정리

일반적인 문제 및 해결 방법

컨테이너 시작 시간 초과

_container = new TestcontainersBuilder<TestcontainersContainer>()
    .WithImage("postgres:latest")
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilPortIsAvailable(5432)
        .WithTimeout(TimeSpan.FromMinutes(2)))
    .Build();

포트가 이미 사용 중

항상 무작위 포트 매핑을 사용하세요:

.WithPortBinding(5432, true) // true = 무작위 공개 포트 할당

컨테이너가 정리되지 않음

올바른 정리를 보장하세요:

public async Task DisposeAsync()
{
    await _connection?.DisposeAsync();
    await _container?.DisposeAsync();
}

테스트가 CI에서는 실패하지만 로컬에서는 성공

CI에 Docker 지원이 있는지 확인하세요:

# GitHub Actions
runs-on: ubuntu-latest # Docker가 사전 설치됨

CI/CD 통합

GitHub Actions

name: Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 9.0.x

    - name: Run Integration Tests
      run: |
        dotnet test tests/YourApp.IntegrationTests \
          --filter Category=Integration \
          --logger trx

    - name: Cleanup Containers
      if: always()
      run: docker container prune -f

성능 팁

  1. 컨테이너 재사용 - 컬렉션 내 테스트 간에 픽스처를 공유
  2. Respawn 사용 - 컨테이너를 다시 만들지 않고 데이터 리셋
  3. 병렬 실행 - TestContainers가 포트 충돌을 자동으로 처리
  4. 경량 이미지 사용 - Alpine 버전이 더 작고 빠름
  5. 이미지 캐시 - Docker는 가져온 이미지를 로컬에 캐시

Individual skills in this repo

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

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

관련 스킬