본문 바로가기
@cayman0312025. 12. 10. 19:44

 

테스트 코드를 세분화 하는 이유

개발 공부를 하면서 생각보다 큰 어려움을 마주하는게 바로 테스트코드 작성이였다. TDD 설계가 뭔지, Mock은 또 언제 사용하는지, 통합테스트는 뭐고 단위 테스트는 뭐고... 실무에서 대규모 프로젝트를 진행해본적이 없고, 개인 프로젝트나 협업 프로젝트라 하더라도 학부생 수준에서 할 수 있는 규모의 프로젝트였으니 "일단 돌아만 가게 만들자! 버그는 나중에 생각해"라는 마인드로 항상 개발을 했었던것 같다. 그런데, 프로젝트의 구조가 커질수록 서비스의 로직이 복잡해질수록 어떻게 테스트 코드를 짜고 로직을 구현해야 좋은 코드일지, 여기서 생각지도 못한 버그가 터졌는데 이걸 어떻게 디버깅할지 테스트 코드 작성의 중요성을 절실히 느끼고 있다. 

 

테스트 용어에 대해서는 여러말이 많지만 기본적으로 실무에서 합의된 공통점은 다음과 같다.

  • 단위 테스트(Unit Test)
    • 시스템의 작은 부분(주로 하나의 클래스나 함수)을 검증한다.
    • 개발자가 작성하고, 테스트 프레임워크(xUnit 계열)을 사용한다.
    • 다른 종류의 테스트보다 빠르게 돌아야한다.
  • 통합 테스트(Intergration Test)
    • 독립적으로 개발된 여러 컴포넌트가 서로 붙어서 잘 동작하는지 검증한다.

보통, 이 둘을 포함해 여러 테스트들을 "하나의 구조물"처럼 생각하는 모델이 많이 사용된다. 아래로 갈 수록 개수는 많고 빠르고, 위로 갈수록 개수는 적고 느려진다.

 


단위 테스트란

정의와 특징

단위 테스트의 공통 요소는 다음과 같다.

  • 작은 범위: 시스템의 작은 부분에 초점을 둔 저수준 테스트
  • 개발자가 작성: 개발자가 자신의 코드와 같은 도구(IDE, 언어)로 작성한다.
  • 빠른 실행 속도
"외부 시스템(DB, 메시지 큐, 외부 API, 파일 시스템)등에 의존하지 않고, 순수한 비즈니스 로직을 검토한다.

좋은 단위 테스트의 형태

일반적으로 단위 테스트는 Arrange–Act–Assert(AAA) 구조를 가지는 것이 좋다.

  1. Arrange: 테스트 대상과 필요한 입력/환경을 준비한다.
  2. Act: 테스트 대상 메서드/함수를 호출한다.
  3. Assert: 결과(리턴 값, 상태 변화, 예외 등)를 검증한다.
@Test
void calculateDiscount_shouldApply10Percent() {
    // Arrange
    Order order = new Order(10000);
    DiscountService sut = new DiscountService();

    // Act
    int result = sut.calculate(order);

    // Assert
    assertEquals(9000, result);
}

언제 Mock를 사용하여 단위 테스트를 만들것인가?

테스트 대상 코드가 외부 의존성을 사용할 때, 그 의존성을 그대로 사용하면 테스트가 느려지고 불안정해진다. 이 때 테스트 더블(Test Double)을 사용하는데 이것이 우리가 흔히 말하는 Mock이다.

 

테스트 더블의 종류는 다음과 같다.

  • Dummy
    • 사용되지 않지만, 파라미터 자리를 채우기 위해 전달하는 객체.
  • Stub
    • 호출에 대해 미리 정해둔 값을 반환하는 객체.
    • “이 메서드가 호출되면 무조건 X를 리턴해” 수준.
  • Fake
    • 실제 구현과 비슷하게 동작하지만, 간단한 구현으로 대체한 버전(예: 인메모리 리포지토리).
  • Spy
    • 호출 정보를 기록해두었다가, 나중에 “이 메서드가 몇 번, 어떤 인자로 호출되었는지” 검증할 수 있게 해주는 객체.
  • Mock
    • “어떻게 호출될지(행동)”에 대해 기대(expectation)를 미리 정의하고, 그 기대가 충족되었는지를 검증하는 객체.

언제 무엇을 사용해야 할까

  • Stub:
    • “이 함수가 이렇게 리턴만 해주면 돼”
    • 예: “UserRepository.findById()가 항상 유저 A를 리턴한다고 가정하고 테스트”
  • Mock/Spy:
    • “이 함수가 정확히 몇 번, 어떤 인자로 호출되는지가 중요해”
    • 예: “메일이 정확히 한 번만 발송되었는지”, “트랜잭션 커밋이 호출되는지”
  • Fake:
    • 간단한 인메모리 구현으로 상태 기반 테스트를 하고 싶을 때
    • 예: InMemoryUserRepository (HashMap으로 구현)

통합 테스트(Intergration Test)

통합 테스트는 독립적으로 개발된 단위들이 서로 연결되었을 때 정상적으로 동작하는지를 검증하는 것이 목표이다.

  • 여러 컴포넌트(컨트롤러 +  서비스 + 리포지토리 + DB)가 함께 돌아가는지를 검증한다.
  • 실제 DB, 메시지 브로커, 외부 API와 연동이 가능하다.
  • 단위 테스트보다 느리고, 세팅이 어렵지만, 그 결과는 현실의 실행상황과 훨씬 가깝다.

스프링부트 공식 문서에서도 통합 테스트에서 실제 환경에 가까운 설정으로 애플리케이션 컴포넌트간 상호작용을 검증한다고 명시하고 있다.


Mock vs 통합 테스트 vs Testcontainers

Mock 기반 단위 테스트

  • DB, 외부 API 등을 Mock/Stub로 대체한다.
  • 빠르고, 실패했을 때의 원인(로직이 문제인지 vs 환경 문제인지)을 좁게 볼 수 있다.
  • 그러나 진짜 DB에서 발생하는 이슈들(쿼리 문법, 인덱스, 트랜잭션, 락 등)을 잡아내지 못한다.

통합 테스트 + In memory DB

  • Repository 계층까지 호출하되, DB는 h2같은 인메모리 DB를 사용한다.
  • 속도가 준수하고, JPA같은 ORM에 대한 기본 검증이 가능해진다.
  • 그러나, 이것 역시 실제 RDB와 동작이 100% 일치하지 않아 예상치못한 이슈가 발생할 수 있다.

통합 테스트 + Testcontainers(실제 DB)

Testcontainers는 간단하게, Docker 컨테이너에서 실제 DB, 메시지 브로커 등을 "일회용"으로 띄워주는 오픈소스 라이브러리이다.

  • 실제 프로덕션에서 사용하는 DB, Kafka, Redis 등과 완전히 동일한 환경으로 테스트가 가능하다.
  • Docker가 필요하고, 테스트 속도가 느리고 CI 환경 설정이 복잡하다는 단점이 존재.

언제 무엇을 어떻게?

테스트 피라미드 관점

  • 단위 테스트: 많고 빠르게 - 비즈니스 로직을 촘촘히 검증한다.
  • 통합 테스트: 적당히 - 중요한 흐름에서 서비스/DB/외부 연동이 제대로 되는지 확인한다.
  • E2E/UI 테스트: 소수 - 실제 배포 환경에 가까운 시나리오 검증을 진행한다.

백엔드 개발 관점

  • 도메인/비즈니스 로직
    • 가능하면 외부 의존성을 분리해서 순수 단위 테스트 작성 (Mock/Stub 최소화)
  • Repository / DB 쿼리 / 트랜잭션 로직
    • Testcontainers로 실제 DB와 통합 테스트 작성
    • 최소한 핵심 쿼리와 트랜잭션 시나리오는 Testcontainers 테스트 권장
  • HTTP API (Controller ~ Service ~ Repository)
    • 외부 시스템(다른 서비스 API, Kafka 등)을 제외하고, 우리 앱 내부만 통합 테스트
    • 외부 시스템은 Testcontainers(예: LocalStack, Kafka 등) 또는 잘 정의된 테스트 더블 사용
  • 외부 시스템과의 진짜 통합
    • 가능한 경우 Testcontainers + LocalStack(AWS), WireMock(HTTP Stub) 등으로 구현

테스트 코드에 관한 기본적인 내용은 위와 같다. 더 자세하게 어떤 테스트 코드가 좋은 테스트 코드인지, 실제 개발에 어떻게 테스트 코드를 작성해야 할지는 다른 포스팅에서 추가로 알아보려 한다.

 

Reference

Spring: https://docs.spring.io/spring-boot/reference/testing/testcontainers.html

martinFowler: https://martinfowler.com/bliki/TestDouble.html

cayman031
@cayman031 :: 그누로그

목차