단위 테스트, 통합 테스트 및 엔드투엔드 테스트

발행: (2026년 2월 5일 오전 01:25 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

위의 링크에 있는 전체 글을 번역하려면, 실제 텍스트 내용이 필요합니다.
해당 글의 본문을 복사해서 제공해 주시면, 원본 형식과 마크다운을 그대로 유지하면서 한국어로 번역해 드리겠습니다.

언제, 어떻게, 어디서 그리고 가장 중요한 자동화된 소프트웨어 테스트

자동화 테스트(automated testing)는 사치가 아니며, «nice‑to‑have»도 아니다. 그것은

  • 피드백 메커니즘,
  • 설계 도구,
  • 시스템 지속 가능성을 위한 안전장치이다.

실제로, 테스트는 보통 세 가지 기본 수준으로 구분된다:

수준설명
Unit Tests가장 작은 논리 단위(메서드, 클래스, 순수 함수)를 테스트한다.
Integration Tests여러 컴포넌트의 협업, 올바른 연결(DI, 매핑, 영속성) 및 실제 인프라와의 통신을 검사한다.
End‑to‑End (E2E) / System Tests최종 사용자나 클라이언트가 보는 것처럼 시스템을 끝에서 끝까지 검증한다.

주의 – 수준은 경쟁 관계가 아니라 보완적이다. 가장 흔한 실수는:

  • 하나의 수준에만 투자하는 것.
  • 그들의 역할을 혼동하는 것.

각각을 하나씩 살펴보자, C# (.NET) 예제로.

1. Unit Tests

무엇을 검사하는가

  • 메서드, 클래스 또는 순수 함수.
  • 다음에 대한 의존성이 없음:
    • 데이터베이스,
    • 파일 시스템,
    • 네트워크,
    • 시계,
    • 랜덤 생성기.

결정적이지 않은 경우, mock한다.

왜 중요한가

  • 빠름 (밀리초).
  • 지속적으로 실행됨 (IDE, CI).
  • 행동을 문서화한다.
  • 좋은 설계를 강제한다(SRP, 낮은 결합도).

유닛 테스트하기 어려운 시스템은 거의 항상 설계가 나쁘다.

도메인 예시

public class Order
{
    public decimal TotalAmount { get; }

    public Order(decimal totalAmount)
    {
        TotalAmount = totalAmount;
    }

    public decimal CalculateDiscount()
    {
        if (TotalAmount >= 1000)
            return TotalAmount * 0.10m;

        if (TotalAmount >= 500)
            return TotalAmount * 0.05m;

        return 0;
    }
}

Unit Test (xUnit)

public class OrderTests
{
    [Fact]
    public void Orders_over_1000_get_10_percent_discount()
    {
        var order = new Order(1200m);
        var discount = order.CalculateDiscount();
        Assert.Equal(120m, discount);
    }

    [Fact]
    public void Orders_between_500_and_999_get_5_percent_discount()
    {
        var order = new Order(600m);
        var discount = order.CalculateDiscount();
        Assert.Equal(30m, discount);
    }

    [Fact]
    public void Orders_below_500_get_no_discount()
    {
        var order = new Order(300m);
        var discount = order.CalculateDiscount();
        Assert.Equal(0m, discount);
    }
}

특징

  • 의존성 없음.
  • 깨끗한 Arrange‑Act‑Assert.
  • 결정적인 결과.

2. Integration Tests

무엇을 검사하는가

  • 여러 컴포넌트의 협업.
  • 올바른 연결(DI, 매핑, 영속성).
  • 실제 인프라와의 통신(예: SQLite, Testcontainers, EF Core, 리포지토리).

여기서는 모두를 mock하지 않는다.

왜 중요한가

  • 유닛 테스트가 잡지 못하는 오류를 포착한다.
  • 다음을 식별한다:
    • 잘못된 매핑,
    • 마이그레이션,
    • 직렬화 문제,
    • 잘못 구성된 서비스.

Works on my machine” 버그는 여기서 사라진다.

예시: Repository + EF Core

public class OrderEntity
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<OrderEntity> Orders => Set<OrderEntity>();

    public AppDbContext(DbContextOptions options)
        : base(options) { }
}

public class OrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(OrderEntity order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }

    public async Task<OrderEntity?> GetByIdAsync(int id)
    {
        return await _context.Orders.Fi

> **Source:** ...

```csharp
ndAsync(id);
    }
}

In‑Memory SQLite를 사용한 통합 테스트

public class OrderRepositoryTests
{
    [Fact]
    public async Task Can_save_and_retrieve_order()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite("Filename=:memory:")
            .Options;

        using var context = new AppDbContext(options);
        context.Database.OpenConnection();
        context.Database.EnsureCreated();

        var repository = new OrderRepository(context);
        var order = new OrderEntity { TotalAmount = 500m };

        await repository.AddAsync(order);
        var savedOrder = await repository.GetByIdAsync(order.Id);

        Assert.NotNull(savedOrder);
        Assert.Equal(500m, savedOrder!.TotalAmount);
    }
}

3. End‑to‑End (E2E) / 시스템 테스트

무엇을 검사하는가

  • 시스템을 끝에서 끝까지 테스트합니다. 최종 사용자 또는 클라이언트가 보는 그대로입니다.
  • 흐름 예시:
HTTP request → controller → business logic → database → response
  • Mock 없이 – 모든 것이 실제입니다.

왜 중요한가 (그리고 위험한가)

✅ 장점❌ 단점
시스템이 실제로 동작한다는 것을 확인합니다.느립니다.
통합이 올바른지 검증합니다.깨지기 쉽습니다.
높은 수준의 신뢰성을 제공합니다.디버깅이 어렵습니다.

실패한 E2E 테스트는 무언가가 깨졌다는 사실을 알려 주지만, 무엇이 깨졌는지는 알려 주지 않습니다.

예시: ASP.NET Core API

Controller

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly AppDbContext _context;

    public OrdersController(AppDbContext context)
    {
        _context = context;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        var order = new OrderEntity
        {
            TotalAmount = request.TotalAmount
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        return Ok(order.Id);
    }
}

WebApplicationFactory를 사용한 E2E 테스트

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

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Can_create_order_via_api()
    {
        var payload = new { TotalAmount = 750 };
        var response = await _client.PostAsJsonAsync("/api/orders", payload);

        response.EnsureSuccessStatusCode();

        var orderId = await response.Content.ReadFromJsonAsync<int>();
        Assert.True(orderId > 0);
    }
}

피라미드 테스트 (Testing Pyramid)

이상적인 테스트 분포는 고전적인 피라미드 모델을 따릅니다:


        │   E2E / System Tests   (전체 수의 5‑10%)

        │   Integration Tests    (10‑20%)

        │   Unit Tests           (70‑85%)
        └───────────────────────────────────────
  • Unit Tests: 기반 – 많고, 빠르며, 신뢰성이 높음.
  • Integration Tests: 중간 레벨 – 적지만 협업을 검증함.
  • E2E Tests: 정점 – 매우 적고 시간 비용이 크지만 가장 높은 수준의 신뢰성을 제공함.

이 구조를 유지하면 다음을 보장할 수 있습니다:

  1. 빠른 피드백 루프 (unit tests).
  2. 통합 단계에서의 오류 탐지 (integration tests).
  3. 최종 기능 검증 (E2E tests).

테스트 유형 및 레벨

케이스테스트 유형
Business rulesUnit
CalculationsUnit
RepositoriesIntegration
EF mappingsIntegration
API wiringIntegration
Happy path 사용자E2E
Smoke testsE2E

경험 규칙

≈ 70 % Unit 테스트

언제 무엇을 사용하나요?

  • Unit tests – 비즈니스 규칙 로직 및 계산을 위해.
  • Integration tests – 외부 의존성(예: 저장소, EF 매핑, API)과의 상호작용을 위해.
  • E2E tests – 전체 사용자 시나리오(해피 패스)와 빠른 검사(스모크)를 위해.

결론 (senior take)

  • Unit testsdesign을 보호합니다.
  • Integration tests인프라를 보호합니다.
  • E2E tests는 시스템에 대한 신뢰를 보호합니다.

어떤 레벨도 다른 레벨을 대체하지 않습니다.

✉️ 연락처: nikosstit@gmail.com

Back to Blog

관련 글

더 보기 »