[Test] Mock(with Mockito)

2025. 7. 5. 18:27·Software Development/Test

가짜로 진짜를 테스트하기: 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); // 메일 발송 이력이 잘 저장되었는지 확인
    }
}
  1. @MockitoBean: 스프링 컨테이너에 진짜 MailSendClient 빈 대신 우리가 만든 가짜(Mock) 객체를 등록한다.
  2. 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으로 대체하면, 우리는 우리 코드의 로직에만 집중하여 안정적인 테스트를 구축할 수 있다.

Ref) Practical Testing: 실용적인 테스트 가이드 강의 | 박우빈 - 인프런

'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
'Software Development/Test' 카테고리의 다른 글
  • [Test] Presentation Layer Test(with Spring Boot)
  • [Test] Business Layer Test(with Spring Boot)
  • [Test] Persistence Layer Test(with Spring Boot, JPA)
  • [Test] 테스트는 '문서'다.
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (456)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (129)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (5)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Test] Mock(with Mockito)
상단으로

티스토리툴바