유닛·통합·E2E·TDD·커버리지
이 장을 읽기 전에: 프론트엔드 또는 백엔드의 기본적인 개발 경험이 있으면 구체적인 예시와 연결하기 쉽다.
테스트의 목적은 코드의 올바름을 증명하는 것이 아니다. 본래의 목적은 안심하고 변경할 수 있는 상태를 만드는 것이다.
이 장에서는 테스트의 종류, 도구 선정, 커버리지, TDD, AI 생성 코드 시대의 검증 방침을 정리한다.
� 한국 시장 맥락: 국내 주요 IT 기업(카카오·네이버·토스·라인)은 Vitest + Playwright 조합(TypeScript 기반)을 신규 프로젝트 표준으로 채택하는 추세다. Java/Kotlin 백엔드는 JUnit5 + Mockito + Testcontainers 조합이 사실상 표준이다. 공공·SI 환경은 JUnit4 레거시와 공존하는 경우가 많다.
이 섹션이 답하는 질문: 어떤 종류의 테스트를, 어디에 두텁게 두어야 하는가?
모든 것을 E2E로 확인하는 것은 느리고 망가지기 쉽다. 반대로 유닛 테스트만으로는 실제 접속 불량을 잡을 수 없다.
유닛 테스트를 토대로 한다 (피라미드의 밑면)
중요한 경계에는 통합 테스트를 둔다
주요 업무 플로만 E2E로 눌러둔다
"뭐든 E2E"도 "뭐든 목(Mock)"도 편향되어 있다. 중요한 것은 망가졌을 때 곤란한 장소에 따라 테스트 종류를 배치하는 것이다.
이 섹션이 답하는 질문: 유닛, 통합, E2E는 어떻게 다른가?
목은 편리하지만, 너무 늘리면 본물과의 어긋남을 숨긴다.
좋은 사용법:
토스페이먼츠·카카오페이 등 외부 과금 API의 고액 호출을 피한다
시각이나 난수를 안정화한다
실패 조건을 의도적으로 만든다
나쁜 사용법:
DB 접속이나 HTTP 경계를 전부 목으로 처리한다
실제의 JSON 직렬화 차분을 보지 않는다
계약 불정합을 목으로 숨긴다
// 좋은 목 사용 예 — 외부 결제 API만 목으로 처리
// 실제 DB는 Testcontainers로 실제 DB를 사용
import { createOrder } from './order-service';
import { PaymentGateway } from './payment-gateway';
// 결제 게이트웨이만 목으로 처리 (실제 호출 방지)
jest.mock('./payment-gateway');
const mockPayment = PaymentGateway as jest.MockedClass<typeof PaymentGateway>;
test('주문 생성 성공 시 결제 게이트웨이 호출', async () => {
mockPayment.prototype.charge.mockResolvedValue({ success: true, txId: 'tx_123' });
const order = await createOrder({ userId: 1, amount: 10000 });
expect(order.status).toBe('PAID');
expect(mockPayment.prototype.charge).toHaveBeenCalledWith({
amount: 10000,
currency: 'KRW',
});
});
목은 테스트를 빠르게 하지만, 현실 그 자체로 만들지 않는다. 경계의 일부는 본물로 확인할 필요가 있다.
이 섹션이 답하는 질문: 어떤 도구를 사용해서 테스트를 작성해야 하는가?
// JUnit5 + Mockito + Testcontainers — Spring Boot 테스트 예시
@SpringBootTest
@Testcontainers
class OrderServiceTest {
@Container
companion object {
@JvmField
val postgres = PostgreSQLContainer<Nothing>("postgres:16")
.apply { withDatabaseName("test_db") }
}
@Autowired
lateinit var orderService: OrderService
@MockBean
lateinit var paymentGateway: PaymentGateway // 결제 게이트웨이만 목
@Test
fun `주문 생성 성공 시 PAID 상태 반환`() {
// given
given(paymentGateway.charge(any()))
.willReturn(PaymentResult(success = true, txId = "tx_123"))
// when
val order = orderService.createOrder(
userId = 1L,
amount = 10000, // 원화
)
// then
assertThat(order.status).isEqualTo(OrderStatus.PAID)
assertThat(order.paymentTxId).isEqualTo("tx_123")
}
}
신규 TypeScript 계열: Vitest
지금 바로 작가의 멤버십 구독자가 되어
멤버십 특별 연재 콘텐츠를 모두 만나 보세요.
오직 멤버십 구독자만 볼 수 있는,
이 작가의 특별 연재 콘텐츠