Spring, Apache, Java
[Stub() / Mock() / Spy()], [@MockitoBean / @Mock] 차이점
cwchoiit
2025. 5. 9. 10:18
728x90
반응형
SMALL
테스트 코드 작성할 때, 비슷비슷해 보이는 것들이 살짝 살짝 다른 차이점을 가지고 있는데, 더이상 헷갈리지 않게 정리를 해보고자 한다.
Stub
- 미리 정해진 값을 리턴하는 고정된 대답 머신이다.
- 진짜 구현을 대체해서 정해진 상황을 흉내만 내는 더미 객체이다.
- 행동 검증보단 상태 기반 테스트에 사용된다.
// 예시: 이메일 서비스가 항상 성공하는 것처럼 흉내내기
when(emailService.send(any())).thenReturn(true);
- "얘한테 뭘 줘도 항상 true를 반환할거야!" 이럴 때 쓰는 용도! (로직 테스트에 집중)
Mock
- 행동 검증용 객체
- 진짜 기능은 없고, "누가 나 호출했어? 몇 번 했어? 어떤 파라미터로 불렸어?" 이런 것을 추적한다.
- 그래서 상호작용 기반 테스트에 자주 사용된다.
verify(emailService, times(1)).send(any());
- "이메일 서비스의 send() 메서드 진짜 호출됐는지 확인하고 싶어!" 이런 상황에 적합하다.
- 여기서 더 나아가, 1번만 호출했고, 파라미터는 어떤것인지도 지정가능하다.
- 위 예시 코드에서는 한번 호출을 검증하고, send(...) 파라미터 any()는 어떤것이든 상관없다는 것을 말한다.
Spy
- 진짜 객체를 감시하고, 원래 메서드는 진짜로 호출되지만, 특정 메서드를 조작할 수 있다.
- 기본은 진짜 동작을 따르고, 필요한 부분만 Stub처럼 바꾸는 것.
BookRepository bookRepository = mock(BookRepository.class);
PushService pushService = mock(PushService.class);
LibraryService libraryService = spy(new LibraryService(bookRepository, pushService));
doReturn(Optional.of("Override spy")).when(libraryService).borrowBook("1234");
- "대부분은 원래대로 동작하되, 이 부분만 살짝 바꿀게!" 할 때 딱이다.
스프링 부트 테스트
스프링 부트 프로젝트에서 Mockito를 사용할땐 두 가지 방법이 있다.
@SpringBootTest + @MockitoBean
실제로, Spring Context를 띄우고, Context에 빈으로 등록하고 기존 빈을 대체하려면 @MockitoBean을 사용하면 된다.
@SpringBootTest
class LibraryServiceTest {
@MockitoBean
BookRepository bookRepository;
@MockitoBean
PushService pushService;
@Autowired
LibraryService libraryService;
@Test
@DisplayName("도서 이용 가능 여부 확인")
void isAvailable() {
when(bookRepository.findBookByIsbn(eq("1234")))
.thenReturn(Optional.of(new Book("1234", "title", true)));
boolean available = libraryService.isAvailable("1234");
assertThat(available).isTrue();
}
}
- LibraryService는 BookRepository, PushService를 주입받아야 한다.
- 이때, 실제 빈을 주입해도 되지만, Mock 객체를 주입할수도 있는데 이럴때 위 코드와 같이 주입 대상에 @MockitoBean 애노테이션을 달아버리면 된다.
만약, Mock 객체말고, Spy 객체를 넣고 싶으면 이렇게 하면 된다.
@SpringBootTest
class LibraryServiceTest {
@MockitoSpyBean
BookRepository bookRepository;
@MockitoBean
PushService pushService;
@Autowired
LibraryService libraryService;
@Test
@DisplayName("도서 이용 가능 여부 확인")
void isAvailable() {
when(bookRepository.findBookByIsbn(eq("1234")))
.thenReturn(Optional.of(new Book("1234", "title", true)));
boolean available = libraryService.isAvailable("1234");
assertThat(available).isTrue();
}
}
- BookRepository는 실제 빈이지만, 내가 원하는 부분을 부분 조작할 수 있다.
@ExtendWith(MockitoExtension.class) + @Mock
스프링 부트 프로젝트이지만, 스프링 부트를 띄우지 않고 단순 Mock 객체만을 사용해서 테스트하고 싶을때도 있다.
@ExtendWith(MockitoExtension.class)
class LibraryServiceTest {
@Mock
BookRepository bookRepository;
@Mock
PushService pushService;
@InjectMocks
LibraryService libraryService;
@Test
@DisplayName("도서 이용 가능 여부 확인")
void isAvailable() {
when(bookRepository.findBookByIsbn(eq("1234")))
.thenReturn(Optional.of(new Book("1234", "title", true)));
boolean available = libraryService.isAvailable("1234");
assertThat(available).isTrue();
}
}
- @ExtendWith(MockitoExtension.class) 애노테이션만 달고 @SpringBootTest 애노테이션은 떼어버린다.
- 그럼 Spring Context가 없고 정말 Pure Java 소스 검증에 사용한다.
- 대신, LibraryService는 BookRepository, PushService를 의존하고 있으니 각 객체를 Mock 객체로 선언하고 해당 Mock 객체를 주입받는다는 표현으로 @InjectMocks 애노테이션을 달아주면 된다.
이 두 가지 방법에는 무슨 차이가 있을까?
방식 | 상황 |
@SpringBootTest + @MockitoBean | 통합 테스트 / 스프링 빈 DI까지 검증하고 싶다 |
@ExtendWith(MocitoExtension.class) + @Mock + @InjectMocks | 빠른 단위 테스트 (스프링을 띄우지 않기 때문에 매우 빠름) |
그리고, 한가지 헷갈릴 요소가 있는데, 다음과 같이 Mock 객체를 생성하면
@MockitoBean
BookRepository bookRepository;
- Mock 객체니까, 위에서 정리한대로 호출됐는지? 몇 번 호출됐는지? 어떤 파라미터로 호출됐는지?를 검증하는것은 당연히 가능하다.
- 그럼, Stub은 안되냐?하면 Stub도 가능하다. Mock 객체로 Stub을 한다고 생각하면 된다.
@Test
@DisplayName("도서 이용 가능 여부 확인")
void isAvailable() {
when(bookRepository.findBookByIsbn(eq("1234")))
.thenReturn(Optional.of(new Book("1234", "title", true)));
boolean available = libraryService.isAvailable("1234");
assertThat(available).isTrue();
}
- Mock 객체(BookRepository)로 Stub 처리를 한 모습이다.
728x90
반응형
LIST