← 개인 공부 · Backend
STUDY NOTE · 개념 정리

유닛 테스트 개념 정리: 테스트 피라미드, Test Double, 좋은 테스트의 조건

Backend
유닛 테스트는 가장 작은 단위(보통 클래스·메서드 하나)를 외부 의존성과 분리해서 의도대로 동작하는지 자동으로 확인하는 테스트다. 빠르고 실패 원인이 명확한 게 핵심이다.

1. 테스트를 쓰는 이유

  1. 회귀 방지: 기능을 고치다가 기존 기능을 깨뜨리는 것을 바로 알 수 있다.
  2. 리팩터링의 안전망: 동작을 보장하는 테스트가 있으면 내부 구조를 자신 있게 바꿀 수 있다.
  3. 살아있는 문서: 테스트 이름과 내용이 "이 코드가 무엇을 해야 하는지"를 보여준다.
  4. 설계 피드백: 테스트하기 어려운 코드는 대개 의존성이 강하게 묶인 코드다.

2. 테스트의 종류

구분유닛 테스트통합 테스트E2E 테스트
범위클래스·메서드 하나여러 컴포넌트 + 실제 DB 등사용자 시나리오 전체
외부 의존성가짜(Test Double)로 대체실제 또는 실제와 유사한 환경실제 환경
속도매우 빠름 (ms)느림 (초)매우 느림
실패 원인 파악쉬움보통어려움
개수가장 많이중간적게

3. 테스트 피라미드

아래로 갈수록 많이, 위로 갈수록 적게 둔다.

  1. 맨 아래 (유닛): 비즈니스 규칙, 계산, 분기를 촘촘하게
  2. 중간 (통합): DB 쿼리, 외부 연동, 계층 간 연결
  3. 맨 위 (E2E): 결제·로그인처럼 깨지면 안 되는 핵심 시나리오 몇 개

위쪽 테스트만 많으면 느리고, 자주 깨지고(flaky), 실패했을 때 어디가 문제인지 알기 어렵다.

4. 테스트 구조: Given-When-Then (AAA)

단계다른 이름하는 일
GivenArrange테스트에 필요한 상태·입력 준비
WhenAct테스트할 동작 실행
ThenAssert결과 검증
@Test
void 연속_5회_실패하면_계정이_잠긴다() {
// given
given(historyRepository.countFailuresSinceLastSuccess("user1")).willReturn(4);
// when
loginService.recordFailure("user1");
// then
verify(accountRepository).lock("user1");
}

5. Test Double: 가짜 객체의 종류

실제 의존 객체 대신 테스트용으로 바꿔 끼우는 객체를 통틀어 Test Double(대역)이라고 한다.

종류설명예시
Dummy자리만 채우고 사용되지 않음생성자에 넘기는 null 대신 객체
Stub정해진 값을 돌려줌 (상태 검증용)조회하면 항상 회원 1명 반환
Spy진짜처럼 동작하면서 호출 기록을 남김메일 발송 횟수 기록
Mock기대하는 호출을 미리 정하고 실제로 호출됐는지 검증 (행위 검증용)lock()이 한 번 호출됐는지 확인
Fake실제로 동작하지만 단순화된 구현메모리 Map으로 만든 리포지토리

Mockito는 Stub과 Mock 역할을 함께 한다. given(...).willReturn(...)이 Stub, verify(...)가 Mock 검증이다.

6. 상태 검증 vs 행위 검증

구분상태 검증행위 검증
확인하는 것실행 후 결과값, 객체 상태의존 객체의 메서드가 호출됐는지
도구assertThatverify
장점내부 구현이 바뀌어도 잘 안 깨짐반환값 없는 동작(발송, 저장) 확인 가능
단점부수효과만 있는 동작은 확인 어려움구현에 묶여 리팩터링 때 자주 깨짐

가능하면 상태 검증을 우선하고, 외부로 나가는 중요한 호출(결제 요청, 메일 발송)만 행위 검증한다.

7. 좋은 유닛 테스트의 조건: FIRST

원칙의미
Fast빠르다. 수백 개가 몇 초 안에 끝나야 자주 돌린다
Independent서로 독립적이다. 실행 순서나 다른 테스트의 결과에 의존하지 않는다
Repeatable언제 어디서 돌려도 결과가 같다. 시간·랜덤·네트워크에 좌우되지 않는다
Self-validating성공·실패가 자동으로 판정된다. 로그를 눈으로 볼 필요가 없다
Timely적시에 작성한다. 기능 구현과 함께, 버그 수정 시 재현 테스트부터

8. 스프링에서의 테스트 범위

방식띄우는 범위용도
순수 JUnit + Mockito스프링 없음서비스·도메인 로직 유닛 테스트
@WebMvcTest웹 계층(컨트롤러, 필터)만요청 매핑, 검증, 응답 코드
@DataJpaTest / @MybatisTest영속성 계층만쿼리, 매핑
@SpringBootTest전체 컨텍스트통합 테스트

슬라이스 테스트에서 빈을 가짜로 바꿀 때는 Spring Boot 3.4부터 @MockBean 대신 @MockitoBean을 쓴다.

9. 실제로 어떻게 적용되나

  1. 경계값 테스트: "5회 실패 시 잠금" 규칙이면 4회(안 잠김)와 5회(잠김)를 모두 테스트한다. 버그는 대부분 경계에서 나온다. @ParameterizedTest로 여러 입력을 한 번에 검증할 수 있다.
  2. 시간에 의존하는 로직: 쿠폰 만료처럼 현재 시각이 필요한 코드는 LocalDateTime.now()를 직접 부르지 않고 Clock을 주입받는다. 테스트에서 시각을 고정할 수 있어 Repeatable 원칙을 지킨다.
  3. 외부 API를 쓰는 서비스: 결제사 클라이언트를 Mock으로 바꿔 "승인 실패 시 주문 상태가 실패로 바뀌는지" 같은 분기를 실제 결제 없이 검증한다.
  4. 쿼리 검증: 리포지토리를 Mock한 유닛 테스트는 SQL이 맞는지 보장하지 않는다. 쿼리는 슬라이스 테스트로 실제(또는 컨테이너) DB에서 따로 확인한다.

10. 테스트하기 쉬운 코드의 특징

  1. 의존성을 생성자로 주입받는다 (메서드 안에서 new 하지 않음)
  2. 시간, 랜덤, 외부 호출 같은 비결정적 요소가 분리돼 있다
  3. 하나의 메서드가 하나의 일만 한다
  4. static 상태나 전역 상태에 의존하지 않는다

11. 면접 질문으로 정리

  1. Q. 유닛 테스트와 통합 테스트의 차이는? 유닛 테스트는 하나의 단위를 외부 의존성 없이 빠르게 검증하고, 통합 테스트는 여러 컴포넌트와 DB 같은 실제 의존성을 함께 붙여 연결이 올바른지 검증한다.
  2. Q. Mock과 Stub의 차이는? Stub은 정해진 응답을 돌려줘 테스트 대상의 결과(상태)를 검증하는 데 쓰고, Mock은 기대한 메서드가 호출됐는지(행위)를 검증하는 데 쓴다.
  3. Q. 좋은 테스트의 조건은? FIRST 원칙(빠르고, 독립적이고, 반복 가능하고, 자동으로 판정되고, 적시에 작성됨)을 만족해야 한다.
  4. Q. 테스트 피라미드란? 빠르고 저렴한 유닛 테스트를 가장 많이, 통합 테스트를 중간, 느리고 비싼 E2E 테스트를 가장 적게 두는 테스트 구성 전략이다.
Gunmo Lee
유닛 테스트 개념 정리: 테스트 피라미드, Test Double, 좋은 테스트의 조건 | Gunmo's Dev Life