[Test_그래도해야지어떡해] #2. Mock, 그리고 Mockito

지난번에 TestNG 에 대해서 공부하다보니, 테스팅 자체에 대한 개념이 부족하다는걸 느꼈다.

그래서 피하고 피해왔던.. 차후 멋진 엔지니어로서 예쁘고 멋진 테스트 코드 작성과 튜닝을 할 수 있는 사람이 되기위해..ㅜ

몰랐던 개념들에 대해 하나하나 짚어보고자 한다.

뭐부터 공부할까 고민하다가 걍 눈에 보이는것들 먼저 짚어보았다.

 

 

#1. Mock 

Mock(모의 객체) 은 말 그대로 “진짜 객체처럼 동작하지만 실제 로직은 수행하지 않는 가짜 객체” 

테스트 대상 코드가 외부 의존성(데이터베이스, 파일, API 등) 없이 독립적으로 검증될 수 있도록 도와줌.

class UserService {
    private EmailService emailService;

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void registerUser(String email) {
        // 1. 사용자 등록 로직
        // 2. 이메일 발송
        emailService.sendWelcomeEmail(email);
    }
}

 

이제 UserService를 테스트하고 싶은데,
여기서 진짜 EmailService를 호출하면 이메일이 실제로 전송될 수도 있다. 
➡️ 테스트 환경에서는 이런 외부 의존(이메일, DB, 서버 호출 등)을 피해야 한다. 

그래서 Mock 객체를 만들어 이렇게 “가짜 EmailService”로 대체한다. 

EmailService mockEmailService = mock(EmailService.class);
UserService userService = new UserService(mockEmailService);

이렇게 하면 실제 이메일은 안 보내고,
테스트는 “이 메서드가 호출되었는지” 같은 동작만 검증할 수 있다. 

 

➡️ Mock을 사용하면 이런 복잡한 의존성 없이,
테스트하고 싶은 코드만 독립적(isolated) 으로 검증할 수 있다. 
즉, “단위 테스트”의 핵심 도구.

 

*단위테스트 : 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트, 나중에 이에 대해서도 포스팅 할 예정.


 

Mock 객체의 정의와 특징

- 모의 객체 (Mock Object) 란, 실제 객체의 행동을 흉내내는 가짜 객체(Test Double)로, 테스트 대상 코드의 동작 검증을 위해 만들어진다.

- 행위 검증에 집중, 메서드가 어떻게 호출 / 몇 번 호출 되는지 , 어떤 인자가 전달되는지 검증이 가능하다.

- Mock 은 테스트 중 예상되는 동작을 정의(stubbing)하고, 검증까지 가능하다.

- 수동으로 구현할 수 있지만, 주로 Mockito와 같은 프레임워크로 생성하며, 모든 메서드를 stub 처리해서 사용할 수 있다.

  • Dummy: 전달만 하고 쓰진 않음(널 채우기용)
  • Fake: 간이 구현(예: 메모리 DB)
  • Stub: “질문에 답만 하는” 고정 응답 객체
  • Mock: 호출 자체(행동)를 검증하기 위한 객체
  • Spy: 실제 객체를 감싸 일부는 진짜로 돌리고 일부만 가로채는 부분 모킹

핵심 차이: Stub은 “무엇을 돌려줄지”, Mock은 “어떻게 불렸는지(행동)”까지 검증.

 

Mock 객체의 활용 목적

 

  • 의존성 격리: DB, 네트워크, 파일, 외부 API 같은 불안정/느린 요소 제거
    • 단위 테스트 독립성 보장 : 외부 시스템과 무관하게 코드 단위만 검증가능.
  • 재현성/속도: 테스트를 빠르고 결정적으로
    • 테스트 속도 향상 : 실제 객체보다 가벼운 mock 을 사용해 반복 실행을 빠르게 처리.
  • 행동 검증: “정말 이 메서드를 이렇게 호출했나?”를 직접 확인
    • 특정 상황 시뮬레이션 :  네트워크 장애, 비정상 입력 등 실제 환경에서 구현 어려운 상황을 재현.
  • 경계 조건 테스트: 성공/예외/타임아웃 등 다양한 시나리오를 쉽게 만들기
    • 개발 단계에서 대상 코드의 의존성이나 외부 컴포넌트가 준비되지 않았을 때 임시로 사용가능.

 

Mock 객채와 행위 기반 테스트 : 

- Mock 객체는 행위 기반 테스트에서 많이 사용한다.

- 어떤 동작이 실제로 발생했는지, 올바른 인자가 전달되었는지 등 상태가 아니라 "실행자체"에 집중한다.

- 예시 : 특정 서비스의 메서드가 몇 번 호출 되었는지 검증하는 등. 

 

Mockito 를 통한 Mock 객체 사용 예시 : 

// Mock 객체 생성
List<String> mockList = Mockito.mock(List.class);

// 예상 동작(stubbing) 설정
Mockito.when(mockList.size()).thenReturn(3);

// 메서드 테스트
int size = mockList.size();  // 3 반환

// 행위 검증
Mockito.verify(mockList).size();

 

@Mock Mock 객체 자동 생성
@InjectMocks Mock 객체를 테스트 대상에 자동 주입
Mockito.verify() 행위 검증 (메서드 호출 체크)

 


 

Mockito 에 대해 함 알아보자~!

#2. Mockito

Mockito는 Java에서 Mock 객체를 쉽게 생성하고 검증할 수 있게 해주는 라이브러리

대표적인 Mock 프레임워크로, Spring Boot나 JUnit 테스트에서 거의 표준처럼 쓰임

 

1) 📘 주요 기능 예시

Mock 생성

EmailService emailService = mock(EmailService.class);
Mock 동작 정의 (stubbing)
when(emailService.sendWelcomeEmail("test@example.com")).thenReturn(true);
when( mockObject.someMethod(param) ).thenReturn(value);

 

“mockObject의 someMethod(param)이 호출되면 value를 반환해라.”

 

List<String> mockList = mock(List.class);

// 리스트의 get(0)이 호출될 때 "hello" 반환
when(mockList.get(0)).thenReturn("hello");

// 테스트 코드
System.out.println(mockList.get(0)); // 출력: hello
System.out.println(mockList.get(1)); // 출력: null (stub 안 했으므로 기본값)

 

then 의 옵션은 여러가지가 있다.

  • thenReturn(value) : 지정된 값을 반환
  • thenThrow(exception) : 예외를 던짐
  • thenAnswer(answer) : 호출 시 동적으로 결과 계산
  • thenCallrealMethod() : mock 이 아니라 진짜 메서드 실행 ( spy 에서 주로 사용 )

 

Mock 호출 검증

verify(emailService).sendWelcomeEmail("test@example.com");

 

JUnit과 함께 사용

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    EmailService emailService;

    @InjectMocks
    UserService userService;

    @Test
    void testRegisterUser() {
        userService.registerUser("abc@example.com");
        verify(emailService).sendWelcomeEmail("abc@example.com");
    }
}

 

 

인자 매칭(Argument Matchers)

any(), eq() 같은 matcher 를 지원해서 인자를 유연하게 매칭할 수 있다.

when(mockList.get(anyInt())).thenReturn("mocked!");
System.out.println(mockList.get(10)); // "mocked!"

 

matcher 의미

anyInt() 아무 int 값
anyString() 아무 문자열
any(Class.class) 아무 객체
eq(value) 특정 값과 정확히 일치

 

 

2) Mockito에서 Mock의 핵심 사용법

1. 생성/주입

@ExtendWith(MockitoExtension.class) // JUnit5 예시, TestNG도 유사
class OrderServiceTest {

  @Mock PaymentGateway paymentGateway;   // 의존성
  @Mock InventoryRepository inventory;   // 의존성

  @InjectMocks OrderService orderService; // 테스트 대상 (의존성 자동 주입)
}

 

2. Stubbing(응답 정의)

when(paymentGateway.pay(any())).thenReturn(Receipt.ok());
when(inventory.reserve(eq("ITEM-1"), anyInt())).thenReturn(true);

// void 메서드는 doThrow / doNothing / doAnswer 사용
doThrow(new IOException("down")).when(paymentGateway).refund(any());

**Spy에서는 실제 호출이 일어나는 걸 막기 위해 doReturn(...).when(spy).method() 권장.

ex)

List<String> spyList = spy(new ArrayList<>());

// ✅ 안전한 방식 — 실제 호출 발생하지 않음
doReturn("mocked").when(spyList).get(0);

System.out.println(spyList.get(0)); // "mocked" 출력, 예외 없음

 

 

3. Verification(호출 검증)

orderService.place("ITEM-1", 2);

verify(inventory).reserve("ITEM-1", 2);     // 호출 여부
verify(paymentGateway, times(1)).pay(any()); // 횟수
verify(paymentGateway, never()).refund(any()); // 호출 안 됨
verifyNoMoreInteractions(inventory, paymentGateway); // 그 외 없음

행동 검증Mock의 존재 이유. (state 검증은 assert로 객체 상태 확인)

 

4. ArgumentCaptor(인자 포착)

ArgumentCaptor<PaymentRequest> captor = ArgumentCaptor.forClass(PaymentRequest.class);

verify(paymentGateway).pay(captor.capture());
assertEquals(2, captor.getValue().quantity());

→ 호출됐을 때 무슨 값이 넘어갔는지까지 확인 가능.

 

5. BDD 스타일

given(inventory.reserve("ITEM-1", 2)).willReturn(true);

orderService.place("ITEM-1", 2);

then(paymentGateway).should().pay(any());

가독성: Given–When–Then 흐름에 잘 맞음.

 

*BDD 역시 다음 글에 적어볼예정..

 

 


#3. TestNG + Mockito 버전으로 

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

public class UserServiceTest {

    @Mock
    private EmailService emailService;  // 모의 객체(Mock)

    private UserService userService;    // 테스트 대상

    private AutoCloseable mocks;        // openMocks() 닫기용

    @BeforeClass
    public void setUp() {
        // Mock 초기화
        mocks = MockitoAnnotations.openMocks(this);
        userService = new UserService(emailService);
    }

    @Test
    public void testRegister_withValidEmail_shouldSendEmailAndReturnSuccess() {
        // Given
        when(emailService.isValid("test@example.com")).thenReturn(true);

        // When
        String result = userService.register("test@example.com");

        // Then
        assertEquals(result, "success");
        verify(emailService, times(1)).send("test@example.com");
    }

    @Test
    public void testRegister_withInvalidEmail_shouldReturnFail() {
        // Given
        when(emailService.isValid("invalid-email")).thenReturn(false);

        // When
        String result = userService.register("invalid-email");

        // Then
        assertEquals(result, "fail");
        verify(emailService, never()).send(anyString());
    }

    @AfterClass
    public void tearDown() throws Exception {
        // Mockito context 해제 (권장)
        mocks.close();
    }
}

 

 

  • @BeforeClass
    → Mockito mock 초기화 및 UserService 생성
  • @Test #1
    → 유효한 이메일 시나리오: isValid() → true, send() 호출 확인
  • @Test #2
    → 잘못된 이메일 시나리오: isValid() → false, send() 호출 안 됨
  • @AfterClass
    → mocks.close()로 context 정리

 

 


여담)

구수한 상인같은 gpt

 

정리해드릴깝쇼? 이런느낌이라서 웃겨서 남김 

 

728x90