Test Double과 Mockito 완벽 가이드

Sang_WJ·2025년 12월 5일

Spring

목록 보기
2/3
post-thumbnail

Test Double과 Mockito 완벽 가이드

좋은 테스트 코드란?

좋은 테스트 코드는 FIRST 규칙을 따릅니다:

  • Fast: 테스트는 빠르게 실행되어야 합니다
  • Independent: 각 테스트는 독립적이어야 합니다
  • Repeatable: 어떤 환경에서도 반복 가능해야 합니다
  • Self-Validating: 테스트는 스스로 성공/실패를 판단해야 합니다
  • Timely: 적시에 작성되어야 합니다

Test Double이란?

개념

Test Double은 "대역"을 의미합니다. 영화 촬영에서 스턴트 더블이 위험한 장면을 대신 연기하듯이, 테스트에서도 실제 객체를 대신하는 객체를 사용합니다.

단위 테스트를 진행할 때 생성하기 까다로운 클래스를 직접 생성(Sociable Test)하지 않고, 이를 대신해 테스트를 진행할 수 있도록 만들어진 객체를 Test Double(Solitary Test)이라고 합니다.

Test Double의 종류

Test Double에는 크게 4가지 종류가 있습니다:

  • Dummy: 파라미터를 채우기 위한 용도
  • Stub: 미리 준비된 답변을 제공
  • Spy: 호출 정보를 기록
  • Mock: 행위를 검증

Test Double 상세 설명

1. Test Stub ⭐️

Stub은 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 객체입니다.

특징:

  • 실제 객체처럼 동작하는 클래스를 직접 구현합니다
  • 테스트에 필요한 구현에만 집중하고 부가적인 기능은 구현하지 않습니다
  • 상태 검증을 사용: 어떤 입력(Input)에 대해 어떤 출력(Output)이 발생하는지 검증합니다
// Stub 예시
class EmailServiceStub implements EmailService {
    @Override
    public boolean send(String email) {
        return true; // 항상 성공을 반환
    }
}

2. Test Spy

Spy 객체는 관점에 따라 2가지로 정의됩니다.

Test Doubles 관점

Stub의 역할을 하면서 부가적인 정보를 기록하는 기능을 가진 객체를 의미합니다.

Mockito 관점

실제 객체를 감싸서 지정된 메서드만 Stub하고, 그렇지 않은 메서드는 Production Code처럼 동작하는 객체를 말합니다.

// Spy 예시
@Spy
UserService userService;

@Test
void spyExample() {
    // 특정 메서드만 Stub
    doReturn("Mocked Name").when(userService).getUserName();

    // 나머지 메서드는 실제 코드 실행
    userService.saveUser(user);
}

3. Test Mock ⭐️

Mock은 실제 객체의 동작을 모방하는 객체입니다.

특징:

  • 메서드 호출에 대한 기대를 명세하고, 미리 정의해놓은 내용에 따라 동작(호출)했는지 검증합니다
  • 행위 검증에 사용됩니다
// Mock 예시
@Mock
EmailService emailService;

@Test
void mockExample() {
    // given
    when(emailService.send(anyString())).thenReturn(true);

    // when
    userService.registerUser(user);

    // then - 행위 검증
    verify(emailService, times(1)).send(user.getEmail());
}

Stub vs Mock - 핵심 차이점

Stub (상태 검증)

  • 목적: 테스트에 필요한 호출에 대해 미리 준비된 답을 제공
  • 구현: 실제 객체처럼 동작하는 클래스를 직접 구현 (테스트에 필요한 부분만)
  • 검증: Input에 대해 어떤 Output이 반환되었는지 검증 → 상태 검증

Mock (행위 검증)

  • 목적: 예상된 동작을 가진 객체
  • 구현: Mock Framework를 통해 객체를 생성하고 호출 기대 명세를 정의
  • 검증: Input/Output보다는 정해진 비즈니스 로직대로 행동했는지 검증 → 행위 검증

예시로 비교하기

// Stub - 상태 검증
@Test
void stubTest() {
    // given
    when(repository.findById(1L)).thenReturn(user);

    // when
    User result = userService.getUser(1L);

    // then - 반환된 결과(상태)를 검증
    assertThat(result.getName()).isEqualTo("John");
}

// Mock - 행위 검증
@Test
void mockTest() {
    // when
    userService.deleteUser(1L);

    // then - 메서드가 호출되었는지(행위)를 검증
    verify(repository, times(1)).deleteById(1L);
}

왜 Test Double을 사용할까?

Test Double을 사용하는 이유는 간단합니다. 바로 주요 "관심사"에 집중하기 위해서입니다.

실제 사례

예를 들어, 사용자 등록 기능을 테스트한다고 가정해봅시다:

  • 실제 데이터베이스 연결이 필요한가? ❌
  • 실제 이메일을 발송해야 하는가? ❌
  • 실제 결제 API를 호출해야 하는가? ❌

우리가 테스트하고 싶은 것은 사용자 등록 로직이 올바르게 동작하는지입니다. 외부 의존성은 Test Double로 대체하여 핵심 로직에만 집중할 수 있습니다.


Mockito 사용법

Mockito는 Java에서 가장 많이 사용되는 Mock Framework입니다.

핵심 어노테이션

@Mock

Mock 객체를 생성합니다.

@Mock
private UserRepository userRepository;

@MockBean

Spring Application Context에 Mock Bean을 등록합니다. (Spring Boot 테스트용)

@MockBean
private EmailService emailService;

@Spy

특정 객체의 메서드를 선택적으로 Stubbing하고, 나머지 메서드는 Production Code처럼 동작하도록 합니다.

@Spy
private UserService userService;

@Test
void spyTest() {
    // 특정 메서드만 Stub
    doReturn(true).when(userService).isAdmin(anyLong());

    // 나머지는 실제 코드 실행
    userService.processUser(1L);
}

@InjectMocks

클래스가 필요로 하는 의존성과 맞는 Mock 객체들을 감지하여, 해당 클래스의 객체가 만들어질 때 Mock 객체를 자동으로 주입해주는 어노테이션입니다.

@Mock
private UserRepository userRepository;

@Mock
private EmailService emailService;

@InjectMocks
private UserService userService; // Mock 객체들이 자동 주입됨

@Test
void injectMocksTest() {
    // userService는 이미 Mock들이 주입된 상태
    when(userRepository.findById(1L)).thenReturn(Optional.of(user));

    userService.registerUser(user);

    verify(emailService).send(user.getEmail());
}

Mockito 기본 사용 패턴

1. 초기화

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;
}

2. Stubbing (동작 정의)

// 메서드 호출 시 특정 값 반환
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// 예외 던지기
when(userRepository.save(any())).thenThrow(new RuntimeException());

// void 메서드 Stubbing
doNothing().when(emailService).send(anyString());

3. 검증 (Verification)

// 메서드가 1번 호출되었는지 검증
verify(userRepository, times(1)).save(user);

// 메서드가 호출되지 않았는지 검증
verify(emailService, never()).send(anyString());

// 메서드가 최소 n번 호출되었는지 검증
verify(userRepository, atLeast(1)).findById(anyLong());

정리

  1. Test Double은 테스트에서 실제 객체를 대신하는 객체입니다
  2. Stub은 상태 검증, Mock은 행위 검증에 사용됩니다
  3. Mockito는 Mock 객체를 쉽게 만들고 검증할 수 있게 해주는 프레임워크입니다
  4. Test Double을 사용하면 핵심 로직에 집중할 수 있습니다
  5. @Mock, @Spy, @InjectMocks 어노테이션을 적절히 활용하세요
profile
성장의 흔적을 남기는 블로그입니다.

0개의 댓글