[워밍업 클럽 3기 BE 클린코드&테스트] - Day 18 미션
1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.
1. @Mock vs @MockBean
@Mock
Mockito에서 제공하는 애너테이션.
해당 필드에 가짜(Mock) 객체를 생성해 준다.
주로 순수한 단위 테스트(단일 클래스 테스트)에서 사용한다.
스프링 컨테이너와는 무관하게 동작하므로, 원하는 경우
@InjectMocks
를 통해 직접 의존성 주입(필드/생성자/Setter)이 일어날 수 있게끔 설정해줘야 한다.
ExtendWith(SpringExtension.class)
class MyServiceTest {
// 1) Repository를 Mock 객체로 만든다.
@Mock
private MyRepository myRepository;
// 2) MyRepository가 필요한 MyService에 @InjectMocks로 Mock을 주입받는다.
@InjectMocks
private MyService myService;
@Test
void testFindById() {
// given
when(myRepository.findById(1L)).thenReturn(Optional.of(new MyEntity(1L, "Test Data")));
// when
MyEntity result = myService.findById(1L);
// then
assertThat(result.getName()).isEqualTo("Test Data");
}
}
@MockBean
Spring Boot에서 제공하는 애너테이션 (org.springframework.boot.test.mock.mockito).
스프링 컨테이너에 Mock 객체로 등록해준다. 즉,
@Autowired
나 다른 Bean들이 해당 Mock 객체를 주입받는다.주로
@SpringBootTest
,@WebMvcTest
등 스프링 컨텍스트가 필요한 통합 테스트에서 사용한다.실제 Bean 대신 MockBean이 컨테이너에서 주입되는 형태이다.
@SpringBootTest
class MyServiceIntegrationTest {
@Autowired
private MyService myService;
// 스프링 컨테이너에 MyRepository를 MockBean으로 등록
@MockBean
private MyRepository myRepository;
@Test
void testFindById() {
// given
when(myRepository.findById(1L)).thenReturn(Optional.of(new MyEntity(1L, "Integration Test")));
// when
MyEntity result = myService.findById(1L);
// then
assertThat(result.getName()).isEqualTo("Integration Test");
}
}
2. @Spy vs @SpyBean
@Spy
Mockito에서 제공하는 애너테이션.
부분(Mock) 테스트에 사용되며, 실제 객체를 부분적으로 Mocking한다.
즉, 스파이(Spy)는 기본적으로 진짜 메서드를 실행하지만, 원하는 메서드만
when(...).thenReturn(...)
형태로 오버라이딩해서 결과를 바꿀 수 있다.@Mock과의 차이는 @Spy는 기본 동작이 실제 객체라는 점이다.
@ExtendWith(SpringExtension.class)
class MyServiceSpyTest {
// Repository 스파이 객체
// => 실제 MyRepository 구현체 인스턴스를 생성 + 원하는 부분만 stub 가능
@Spy
private MyRepository myRepository = new MyRepositoryImpl();
// 스파이 객체를 주입받을 MyService
@InjectMocks
private MyService myService;
@Test
void testFindByIdWithSpy() {
// spy는 실제 동작을 수행하므로,
// 특정 메서드만 오버라이딩해서 동작을 가짜로 만들 수 있다.
doReturn(Optional.of(new MyEntity(2L, "Spy Data")))
.when(myRepository).findById(2L);
// 2L의 경우에는 Stub이 적용되어 Mock 동작,
// 1L의 경우에는 실제 MyRepositoryImpl 동작.
MyEntity result1 = myService.findById(1L); // 실제동작
MyEntity result2 = myService.findById(2L); // Stub으로 오버라이딩
assertThat(result1).isNotNull(); // 실제 Repo 동작 결과
assertThat(result2.getName()).isEqualTo("Spy Data"); // 오버라이딩된 결과
}
}
@SpyBean
Spring Boot에서 제공.
@MockBean과 유사하지만, 실제 스프링 Bean을 스파이로 만들어 컨테이너에 등록한다.
즉, 컨테이너가 관리하는 Bean을 부분 Mocking하고 싶은 경우에 사용.
다른 Bean들이 해당 @SpyBean을 주입받아 사용할 때는, 기본적으로 진짜 로직을 타게 되지만 원하는 메서드만 Stub 할 수 있다.
@SpringBootTest
class MyServiceSpyBeanTest {
// MyService는 진짜 Bean
@Autowired
private MyService myService;
// MyRepository를 스파이 Bean으로 등록
@SpyBean
private MyRepository myRepository;
@Test
void testFindByIdWithSpyBean() {
// 실제 Bean이지만 특정 메서드를 Stub
doReturn(Optional.of(new MyEntity(999L, "SpyBean Data")))
.when(myRepository).findById(999L);
MyEntity result = myService.findById(999L);
// 999L일 때만 Stub 동작 → "SpyBean Data"
assertThat(result.getName()).isEqualTo("SpyBean Data");
}
}
3. @InjectMocks
Mockito에서 제공하는 애너테이션.
@Mock 또는 @Spy로 만들어진 Mock/Spy 객체들을 해당 클래스(필드, 생성자 등)에 자동 주입해 준다.
스프링과는 무관하게 Mockito 레벨에서만 동작하며, 생성자 주입/필드 주입/Setter 주입 방식으로 의존성을 주입해 준다.
@ExtendWith(SpringExtension.class)
class MyServiceTest {
@Mock
private MyRepository myRepository;
// MyService 생성 시 @Mock으로 만든 myRepository가 자동 주입됨
@InjectMocks
private MyService myService;
@Test
void testSomeLogic() {
// ...
}
}
2. 아래 3개의 테스트가 있습니다.
내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요?
(@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)
/***
* ✔️ 게시판 게시물에 달리는 댓글을 담당하는 Service Test
* ✔️ 댓글을 달기 위해서는 게시물과 사용자가 필요하다.
* ✔️ 게시물을 올리기 위해서는 사용자가 필요하다."
*
*/
class Test {
@BeforeEach
void setUp() {
// 사용자 생성에 필요한 내용 준비 (1-1, 2-1, 3-1)
// 사용자 생성 (1-2, 2-2, 3-2)
// 게시물 생성에 필요한 내용 준비 (1-3, 2-3, 3-5)
// 게시물 생성 (1-4, 2-4, 3-6)
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@org.junit.jupiter.api.Test
void writeComment() {
// given
// 1-5. 댓글 생성에 필요한 내용 준비
// when
// 1-6. 댓글 생성
// then
// 검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@org.junit.jupiter.api.Test
void updateComment() {
// given
// 2-6. 댓글 생성
// when
// 2-7. 댓글 수정
// then
// 검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@org.junit.jupiter.api.Test
void cannotUpdateCommentWhenUserIsNotWriter() {
// given
// 3-3. 사용자2 생성에 필요한 내용 준비
// 3-4. 사용자2 생성
// 3-7. 사용자1의 댓글 생성에 필요한 내용 준비
// 3-8. 사용자1의 댓글 생성
// when
// 3-9. 사용자2가 사용자1의 댓글 수정 시도
// then
// 검증
}
}
댓글을 작성해보세요.