단위 테스트, 통합 테스트 및 엔드투엔드 테스트
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: 정점 – 매우 적고 시간 비용이 크지만 가장 높은 수준의 신뢰성을 제공함.
이 구조를 유지하면 다음을 보장할 수 있습니다:
- 빠른 피드백 루프 (unit tests).
- 통합 단계에서의 오류 탐지 (integration tests).
- 최종 기능 검증 (E2E tests).
테스트 유형 및 레벨
| 케이스 | 테스트 유형 |
|---|---|
| Business rules | Unit |
| Calculations | Unit |
| Repositories | Integration |
| EF mappings | Integration |
| API wiring | Integration |
| Happy path 사용자 | E2E |
| Smoke tests | E2E |
경험 규칙
≈ 70 % Unit 테스트
언제 무엇을 사용하나요?
- Unit tests – 비즈니스 규칙 로직 및 계산을 위해.
- Integration tests – 외부 의존성(예: 저장소, EF 매핑, API)과의 상호작용을 위해.
- E2E tests – 전체 사용자 시나리오(해피 패스)와 빠른 검사(스모크)를 위해.
결론 (senior take)
- Unit tests는 design을 보호합니다.
- Integration tests는 인프라를 보호합니다.
- E2E tests는 시스템에 대한 신뢰를 보호합니다.
어떤 레벨도 다른 레벨을 대체하지 않습니다.
✉️ 연락처: nikosstit@gmail.com