| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 가비지컬렉션
- 자바
- 파이썬
- 코딩테스트팁
- 자바프로그래밍
- 정렬
- 백준
- 객체지향
- Java
- 코딩테스트
- 프로그래머스
- 알고리즘공부
- 알고리즘
- 예외처리
- HashMap
- 코딩인터뷰
- 멀티스레드
- 개발공부
- 메모리관리
- 코딩공부
- 클린코드
- 자료구조
- 자바개발
- 개발자취업
- JVM
- 프로그래밍기초
- 코딩테스트준비
- 개발자팁
- 자바공부
- 자바기초
- Today
- Total
코드 한 줄의 기록
[자바/Mockito] 테스트 더블 완벽 정리: Mock과 Stub 차이부터 BDD까지 본문
안녕하세요! 오늘은 최근 제가 Java로 전향하면서 가장 재미있으면서도, 처음에 꽤나 헷갈렸던 테스트(Test) 관련 이야기를 해보려 합니다.
PHP를 다루다가 Java, 특히 Spring Boot 환경으로 넘어오니 JUnit과 Mockito라는 조합이 거의 공식처럼 쓰이더군요. "그냥 가짜 객체 만들어서 돌리면 되는 거 아니야?"라고 가볍게 생각했다가, Stub, Mock, Spy, Dummy... 쏟아지는 용어들 때문에 머리가 지끈거렸던 경험, 다들 있으시죠?
오늘은 제가 공부하며 정리한 Test Double(테스트 더블) 의 개념과, 자바 진영의 De-facto(사실상의 표준) 라이브러리인 Mockito의 핵심 사용법을 아주 상세하게 파헤쳐 보려 합니다. 저처럼 "도대체 Mock이랑 Stub이 뭐가 다른 거야?"라고 고민하셨던 분들에게 이 글이 명쾌한 해답이 되길 바랍니다.
왜 우리는 '가짜'가 필요할까?
단위 테스트(Unit Test)를 작성하다 보면 필연적으로 의존성(Dependency) 문제에 부딪힙니다.
예를 들어, MemberService의 회원 가입 로직을 테스트하고 싶다고 가정해 봅시다. 그런데 이 서비스는 내부적으로 MailSender를 통해 가입 축하 메일을 보냅니다. 테스트 코드를 돌릴 때마다 실제 메일이 발송된다면 어떻게 될까요?
- 속도 저하: 네트워크를 타기 때문에 테스트가 느려집니다.
- 비용 발생: 메일 발송 서비스가 유료라면 돈이 듭니다.
- 결정적이지 않음(Non-deterministic): 메일 서버가 다운되면, 내 로직은 멀쩡한데 테스트가 실패합니다.
이런 상황을 피하기 위해 우리는 실제 객체 대신 '가짜 객체' 를 사용합니다. 영화 촬영장에서 배우 대신 위험한 연기를 하는 '스턴트 더블(Stunt Double)'처럼, 테스트에서도 실제 객체 대신 투입되는 대역을 테스트 더블(Test Double) 이라고 부릅니다.
테스트 더블(Test Double)의 5가지 계급
제라드 메스자로스(Gerard Meszaros)가 정의한 테스트 더블에는 5가지 종류가 있습니다. 이 용어들을 명확히 구분하는 것부터가 Mockito 정복의 시작입니다.
Dummy (더미)
- 역할: 그냥 자리를 채우기 위한 객체입니다.
- 특징: 실제로 절대 사용되지 않습니다. 메서드가 호출되지도 않고, 호출되더라도 아무 일도 하지 않습니다.
- 예시: 생성자의 파라미터 개수를 맞추기 위해 넘기는
null이나 빈 객체.
// 그냥 자리만 채우는 용도
List<String> dummyList = new ArrayList<>();
MemberService service = new MemberService(dummyList);
Fake (페이크)
- 역할: 동작은 하지만, 프로덕션(실제)에 쓰기엔 부적합한 객체입니다.
- 특징: 복잡한 로직을 단순화하여 구현한 구현체입니다.
- 예시: 실제 DB 대신
HashMap이나ArrayList를 사용한FakeRepository.
public class FakeMemberRepository implements MemberRepository {
private Map<Long, Member> storage = new HashMap<>();
@Override
public void save(Member member) {
storage.put(member.getId(), member);
}
}
Stub (스텁)
- 역할: 미리 준비된 답변(Answer)을 하는 객체입니다.
- 특징: 호출되면 설정된 고정 값을 리턴합니다.
- 핵심: 상태 검증(State Verification)에 주로 사용됩니다.
Spy (스파이)
- 역할: 실제 객체를 감시하면서, 필요할 때만 가짜 행세를 하는 객체입니다.
- 특징: 기본적으로는 실제 객체의 메서드를 실행합니다.
- 비유: "평소엔 직원처럼 일하다가, 사장님이 '몇 번 통화했어?'라고 물으면 기록을 보여주는 스파이".
Mock (목)
- 역할: 행위(Behavior)를 검증하기 위한 객체입니다.
- 특징: 수신한 요청이 예상대로 들어왔는지 검증에 초점.
- 핵심: 행위 검증(Behavior Verification).
Stub vs Mock 차이
- Stub: "상태"에 관심 있음.
- Mock: "행위"에 관심 있음.
Mockito 시작하기: 설정과 기본 사용법
Mockito는 테스트 더블을 쉽게 생성하는 자바 표준 라이브러리입니다. spring-boot-starter-test에 포함되어 있습니다.
Mock 객체 만들기
방법 1: mock() 메서드 사용
import static org.mockito.Mockito.mock;
class MemberServiceTest {
@Test
void createMember() {
MemberRepository memberRepository = mock(MemberRepository.class);
}
}
방법 2: 어노테이션 사용
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@Mock
private MemberRepository memberRepository;
@InjectMocks
private MemberService memberService;
@Test
void createMember() {
// memberRepository는 이미 Mock 객체임
}
}
@Mock: 가짜 객체 생성@InjectMocks: Mock 객체를 주입
Stubbing: "이렇게 물어보면 저렇게 대답해"
Mock 객체는 기본적으로 아무 동작을 하지 않습니다. 우리가 원하는 동작을 정의하려면 Stubbing이 필요합니다.
// ID가 1L인 멤버를 찾으면, member 객체를 반환
when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
// 예외 발생
when(memberRepository.findById(-1L))
.thenThrow(new IllegalArgumentException("Invalid ID"));
BDDMockito 스타일
import static org.mockito.BDDMockito.given;
given(memberRepository.findById(1L))
.willReturn(Optional.of(member));
MemberResponse response = memberService.getMember(1L);
assertThat(response.getName()).isEqualTo("테스트유저");
Verification: "너 아까 걔랑 통화했어?"
verify(memberRepository, times(1)).save(any(Member.class));
verify(mailSender, never()).send(anyString());
verify(logger, atLeast(2)).log(anyString());
BDD 스타일 검증
import static org.mockito.BDDMockito.then;
then(memberRepository).should(times(1)).save(any(Member.class));
Mock vs Spy: 언제 무엇을 쓸까?
Mock 객체
List listMock = mock(ArrayList.class);
listMock.add("A");
System.out.println(listMock.size()); // 0
verify(listMock).add("A");
Spy 객체
List listSpy = spy(new ArrayList<>());
listSpy.add("A");
System.out.println(listSpy.size()); // 1
given(listSpy.size()).willReturn(100);
System.out.println(listSpy.size()); // 100
✅ 팁: 대부분의 단위 테스트에서는 Mock 사용을, 일부 실제 동작이 필요한 테스트에서만 Spy를 사용하세요.
Mockito 사용 시 주의사항
any() 사용 시 주의점
// (X)
verify(service).update(1L, any(String.class));
// (O)
verify(service).update(eq(1L), any(String.class));
과도한 Mocking은 독이다
- DTO, Value Object: 실제 생성해서 사용.
- Stubbing이 너무 많다면: 설계 점검 필요.
UnnecessaryStubbingException
lenient().when(repo.findById(any())).thenReturn(Optional.empty());
ArgumentCaptor
@Captor
ArgumentCaptor<Member> memberCaptor;
@Test
void captorTest() {
memberService.join("userA");
verify(memberRepository).save(memberCaptor.capture());
Member capturedMember = memberCaptor.getValue();
assertThat(capturedMember.getName()).isEqualTo("userA");
}
테스트는 개발자의 든든한 보험
Mockito는 단순히 가짜 객체를 만드는 도구가 아닙니다. "내 코드가 외부 세상(DB, API 등)과 어떻게 대화해야 하는지" 를 정의하고 검증해 주는 훌륭한 설계 도구이기도 합니다.
오늘 정리한 내용이 여러분의 테스트 코드 작성에 도움이 되길 바랍니다. 다음에는 JUnit5 심화나 MockMvc를 활용한 컨트롤러 테스트를 다뤄보겠습니다.
궁금한 점이 있다면 댓글로 남겨주세요. 같이 공부하면서 성장해요!
Java 테스트 코드 작성법: Given-When-Then 패턴과 픽스처 관리 완벽 가이드
이 글을 쓰게 된 이유는 최근 제가 Java로 복잡한 비즈니스 로직을 테스트할 때 많은 문제에 부딪혔기 때문입니다. 처음에는 테스트 코드를 무작정 작성했는데, 시간이 지나면서 테스트 코드 자
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| Java 유닛 테스트 완벽 정리: JUnit 5와 단언(Assertions) 개념부터 실전까지 (0) | 2025.12.30 |
|---|---|
| Gradle 기본기 완벽 정리: DSL, Task, Dependency 실무 가이드 (0) | 2025.12.29 |
| Java 테스트 코드 작성법: Given-When-Then 패턴과 픽스처 관리 완벽 가이드 (0) | 2025.12.28 |
| Java 코드 커버리지 메트릭, 진짜 알고 사용하고 있나요? (0) | 2025.12.21 |
| Java 객체 생명주기와 Escape 분석, 박싱 비용 완벽 이해하기 (0) | 2025.12.14 |