가짜로 진짜를 테스트하기: Mock
어드민 페이지에 '오늘 자 매출 통계 메일 전송' 기능을 추가한다고 해보자. 이 기능을 테스트하려면 OrderStatisticsService
를 실행해야 한다.
// OrderStatisticsService.java
public boolean sendOrderStatisticsMail(LocalDate orderDate, String email) {
// 1. 해당 일자의 결제완료된 주문들을 가져온다.
// 2. 총매출 합계를 계산한다.
// 3. 메일을 전송한다.
boolean result = mailService.sendMail(..., email, ..., ...);
if (!result) {
throw new IllegalArgumentException("매출 통계 메일 전송에 실패했습니다.");
}
return true;
}
그런데 이 mailService
내부의 MailSendClient
는 실제 메일 서버로 네트워크 요청을 보내고, 심지어 지금은 무조건 예외를 던지도록 되어 있다. 테스트할 때마다 진짜 메일을 쏠 수도 없는 노릇이고, 외부 API가 불안정하면 내 코드와 상관없이 테스트가 깨질 수 있다.
이럴 때 등장하는 해결사가 바로 Mock(가짜 객체) 이다.
외부 세계 길들이기: @MockitoBean
우리는 테스트 환경에서 진짜 MailSendClient
대신, 우리가 원하는 대로만 동작하는 가짜 MailSendClient
를 사용할 것이다. 스프링 부트 환경에서는 @MockitoBean
으로 간단하게 해결할 수 있다.
@SpringBootTest
class OrderStatisticsServiceTest {
@Autowired
private OrderStatisticsService orderStatisticsService;
// ... 다른 Repository들 ...
@MockitoBean // (1)
private MailSendClient mailSendClient;
@DisplayName("결제완료 주문들의 매출 통계 메일을 전송한다.")
@Test
void sendOrderStatisticsMail() {
// given
// ... 테스트 데이터 생성 ...
// (2) Stubbing: mailSendClient야, 어떤 인자로 sendEmail이 호출되든 무조건 true를 반환해!
when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
.thenReturn(true);
// when
boolean result = orderStatisticsService.sendOrderStatisticsMail(LocalDate.of(2023, 3, 5), "test@test.com");
// then
assertThat(result).isTrue();
List<MailSendHistory> histories = mailSendHistoryRepository.findAll();
assertThat(histories).hasSize(1); // 메일 발송 이력이 잘 저장되었는지 확인
}
}
@MockitoBean
: 스프링 컨테이너에 진짜MailSendClient
빈 대신 우리가 만든 가짜(Mock) 객체를 등록한다.when(...).thenReturn(...)
: 가짜 객체가 어떻게 행동할지 정의하는 과정, 즉 스터빙(Stubbing) 이다.anyString()
은 '어떤 문자열이든 상관없다'는 의미의 Matcher다.
이제 외부 API의 상태와 상관없이, 우리는 OrderStatisticsService
의 로직에만 집중해서 안정적으로 테스트할 수 있게 되었다.
파고들기: Mock은 Stub이 아니다
테스트 코드에서 사용하는 가짜 객체를 통틀어 테스트 더블(Test Double) 이라고 부른다. (위험한 연기를 대신하는 스턴트 더블처럼)
테스트 더블에는 여러 종류가 있지만, 가장 많이 헷갈리는 것이 Stub과 Mock의 차이다. 둘 다 "요청에 대해 미리 준비된 결과를 반환하는 가짜 객체"라는 점은 비슷하지만, 검증의 목적이 다르다.
- Stub: 상태 검증(State Verification) 에 사용된다.
when
블록에서 어떤 동작을 시킨 후,then
블록에서 "그래서 객체의 상태가 어떻게 변했는데?"를 검증한다.- ex)
mailer.numberSent()
가 1인지,list.size()
가 1 증가했는지 확인.
- Mock: 행위 검증(Behavior Verification) 에 사용된다.
then
블록에서 "그래서 그 메서드가 정확히 어떻게 호출됐는데?"를 검증한다.- ex)
mailer.send()
가 정확히 1번 호출되었는지 확인.
순수 Mockito로 단위 테스트하기
@MockitoBean
은 스프링 통합 테스트용이다. 순수 단위 테스트에서는 @ExtendWith(MockitoExtension.class)
와 @Mock
, @InjectMocks
를 사용해 동일한 작업을 할 수 있다.
@ExtendWith(MockitoExtension.class) // JUnit5에서 Mockito를 사용하겠다고 선언
class MailServiceTest {
@Mock // 가짜 MailSendClient
private MailSendClient mailSendClient;
@Mock // 가짜 MailSendHistoryRepository
private MailSendHistoryRepository mailSendHistoryRepository;
@InjectMocks // @Mock 객체들을 주입받을 실제 테스트 대상
private MailService mailService;
@DisplayName("메일 전송 테스트")
@Test
void sendMail() {
// given
when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
.thenReturn(true);
// when
boolean result = mailService.sendMail("", "", "", "");
// then
assertThat(result).isTrue();
// mailSendHistoryRepository의 save()가 MailSendHistory 타입으로 딱 1번 호출되었는지 검증 (행위 검증)
verify(mailSendHistoryRepository, times(1)).save(any(MailSendHistory.class));
}
}
특수 요원 @Spy
가끔은 객체 전체가 아닌, 특정 메서드 하나만 가짜로 만들고 싶을 때가 있다. 이럴 때 @Spy
를 쓴다. Spy는 실제 객체를 기반으로 동작하며, 우리가 지정한 메서드만 Stubbing된다.
주의!
@Spy
를 Stubbing할 때는when(...)
대신doReturn(...).when(spy객체).메서드()
구문을 써야 한다.when
을 쓰면 실제 메서드가 호출되어 버리기 때문이다.
@Spy
private MailSendClient mailSendClient;
// ...
doReturn(true)
.when(mailSendClient) // when()의 인자로 spy 객체를 넘긴다.
.sendEmail(anyString(), anyString(), anyString(), anyString());
더 깔끔하게: BDDMockito
given-when-then
패턴을 쓸 때, given
블록에 when()
이 들어가니 어색하다. Mockito는 이를 위해 이름만 바꾼 BDDMockito
를 제공한다. 기능은 완전히 똑같다.
// Mockito.when(...).thenReturn(...)
// BDDMockito.given(...).willReturn(...)
import static org.mockito.BDDMockito.*;
//...
// given
given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
.willReturn(true);
그래서, 언제 Mock을 써야 할까? (Classicist vs Mockist)
- Mockist (고립주의자): "내가 테스트하는 단위 외의 모든 의존성은 Mock으로 대체해야 해!"
- Classicist (고전주의자): "실제 객체들의 상호작용을 테스트해야 진짜지. Mocking은 외부 시스템처럼 통제 불가능한 경계에서만 최소한으로 써야 해."
정답은 없다. 하지만 일반적으로 우리 시스템의 경계를 벗어나는 지점에서 Mocking은 매우 유용하다. 외부 API 호출, 메일 서버, 파일 시스템 등 우리가 제어할 수 없는 영역을 Mock으로 대체하면, 우리는 우리 코드의 로직에만 집중하여 안정적인 테스트를 구축할 수 있다.
'Software Development > Test' 카테고리의 다른 글
[Test] Presentation Layer Test(with Spring Boot) (0) | 2025.07.05 |
---|---|
[Test] Business Layer Test(with Spring Boot) (0) | 2025.07.05 |
[Test] Persistence Layer Test(with Spring Boot, JPA) (0) | 2025.07.05 |
[Test] 테스트는 '문서'다. (0) | 2025.07.05 |
[Test] TDD(Test Driven Development) (0) | 2025.07.05 |