지금까지 단순히 레이어를 테스트하는데에 있어 @SpringBootTest 어노테이션을 만을 사용해 테스트를 진행했었습니다.
그러다보니 애플리케이션의 규모가 커짐에 따라 테스트 속도가 현저히 떨어져 개발 생산성이 점점 저하되는 상황에 놓이게 되었습니다.
이 문제를 해결하기 위해 레이어별로 독립적으로 테스트할 수 있는 방법(단위 테스트)에 대해 찾아봄으로써, 다양한 슬라이싱 테스트를 전용 어노테이션과 mockito에 대해 알게되어 이 정보를 공유하고자 합니다.
애플리케이션을 운영하는데에 필요한 모든 설정정보 및 Bean을 생성하기 때문에 애플리케이션의 규모에 따라 시간이 오래걸리고 무겁습니다.
그렇기 때문에 디버깅하는데에 있어 시간이 오래걸리고, 이는 테스트 코드를 통한 빠른 피드백의 장점을 희석시킬 수 있게 됩니다.
위 어노테이션의 사용이 필요하다면 꼭 사용을 해야만 하는지에 대해 검토해본 후 사용할 것을 권장합니다.
우선 Slicing Test를 하는데에 있어 Mockito가 지원하는 Mocking 방법에 대해 하나씩 살펴보도록 하겠습니다.
위 메소드는 해당 클래스 타입의 객체를 Mocking해 Proxy 객체를 반환해줍니다.
Mocking된 인스턴스를 사용할 경우 실제 객체의 메소드가 동작하지 않고 사용자가 따로 정의한 로직에 맞게 기능이 동작하게 됩니다.
예시 코드)
PostRepository postRepository;
PostService postService;
@BeforeEach
void setUp(){
postRepository = Mockito.mock(PostRepository.class);
postService = new PostService(postRepository, new PostMapper());
}
@Test
void 게시글_정보_조회() {
// given
when(postRepository.findById(1L))
.thenReturn(Optional.of(new PostResource(1L, "title", "content", "writer")));
// when
PostResource postResource = postService.getPost(1L);
// then
assertNotNull(postResource);
assertEquals(postResource.getId(), 1L);
assertEquals(postResource.getTitle(), "title");
assertEquals(postResource.getContent(), "content");
assertEquals(postResource.getWriter(), "writer");
}
Mockito.mock() 메소드를 사용하지 않고 어노테이션 기반으로 Mocking 할 수 있도록 지원하는 어노테이션입니다.
@Mock 어노테이션을 사용하기 위해서는 @ExtendWith(MockitoExtension.class)을 클래스 레벨에 추가해주어야합니다.
예시 코드)
@ExtendWith(MockitoExtension.class)
class PostRepositoryTest_1 {
@Mock
PostRepository postRepository;
@InjectMocks
PostService postService;
@Test
void 게시글_조회시_존재하지_않을_경우() {
// given
when(postRepository.findById(1L))
.thenReturn(Optional.empty());
// when
assertThrows(PostNotFoundException.class,() -> {
postService.getPost(1L);
});
}
}
위 소스코드를 보면 @InjectMocks 어노테이션을 사용하는 것을 볼 수 있는데, @InjectMocks 어노테이션은 해당 객체를 사용하는데에 있어 그 객체가 내부에서 필요한 객체들(필드로 갖는 객체들)에 대해 @Mock 어노테이션이 붙어있는 객체가 있는지 확인한 후 해당 객체에 주입시켜주는 어노테이션이라고 이해하시면 됩니다. 마치 Spring에서 @Autowired와 비슷한 느낌이라고 보시면 될 것 같습니다.
@MockBean 어노테이션은 Spring 애플리케이션을 구동할때 해당 어노테이션이 붙어있는 객체는 실제 Bean이 아닌 Mocking된 Bean으로써 사용하겠다는 의미입니다.
예시 코드)
@ExtendWith(SpringExtension.class)
class PostServiceTest_2 {
@MockBean
PostService postService;
@Test
void 게시글_정보_조회() {
when(postService.getPost(1L))
.thenReturn(new PostResource(1L, "title", "content", "writer"));
// when
PostResource postResource = postService.getPost(1L);
// then
assertNotNull(postResource);
assertEquals(postResource.getId(), 1L);
assertEquals(postResource.getTitle(), "title");
assertEquals(postResource.getContent(), "content");
assertEquals(postResource.getWriter(), "writer");
}
}
해당 어노테이션을 사용하기 위해서는 @ExtendWith(SpringExtension.class)을 클래스 레벨에 추가해주어야합니다.
위 메소드는 Mockito.mock() 메소드와는 다르게 객체 자체를 Mocking 하는 것이 아닌 필요한 특정 메소드만 Mocking 할때 사용합니다.
특정 메소드만을 Mocking 처리 하기 때문에 Mocking된 메소드를 제외한 다른 메소드들은 Mocking 되지 않고 기존 로직 그대로 정상적으로 수행됩니다.
@SpyBean 어노테이션은 @MockBean 어노테이션과는 다르게 해당 객체 자체를 Mocking 하는 것이 아닌 필요한 특정 메서드만 Mocking 할때 사용하는 어노테이션입니다. 특정 메서드만을 Mocking하기 때문에 Mocking된 메서드를 제외한 다른 메서드들은 Mocking 되지 않고 기존 로직 그대로 정상적으로 수행됩니다.
예시 코드)
@ExtendWith(SpringExtension.class)
class PostServiceTest_3 {
@MockBean
PostRepository postRepository;
@MockBean
PostMapper postMapper;
@SpyBean
PostService postService;
@Test
void 게시글_정보_조회() {
when(postRepository.findById(1L))
.thenReturn(Optional.of(new PostResource(1L, "title", "content", "writer")));
// when
PostResource postResource = postService.getPost(1L);
// then
assertNotNull(postResource);
assertEquals(postResource.getId(), 1L);
assertEquals(postResource.getTitle(), "title");
assertEquals(postResource.getContent(), "content");
assertEquals(postResource.getWriter(), "writer");
}
}
또한 @SpyBean 어노테이션은 기존 Bean을 기반으로 동작하기 때문에 해당 객체에 대한 Bean이 반드시 존재해야하며, 해당 객체가 Bean으로 등록되기 위해 필요한 Bean들 또한 모두 세팅되어 있어야합니다.
테스트를 진행하는 동안 Presentation(Controller) Layer의 Bean들만 생성합니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@OverrideAutoConfiguration(
enabled = false
)
@TypeExcludeFilters({WebMvcTypeExcludeFilter.class})
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
...
}
예시 코드)
@WebMvcTest(PostController.class)
class PostControllerTest_2 {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private PostService postService;
@Test
void 게시글_등록() throws Exception {
// given
WritePostRequest writePostRequest = WritePostRequest.of("title", "content");
when(postService.write(writePostRequest.toDto(), "user"))
.thenReturn(1L);
// when
mockMvc.perform(post("/api/v1/post")
.header("userId","user")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(writePostRequest)))
// then
.andExpect(status().isCreated());
}
}
@WebMvcTest를 사용할 경우 Service 계층의 Bean은 등록되지 않기 때문에 에러가 발생합니다.
그렇기 때문에 해당 컨트롤러에서 필요한 Service에 대해 Mocking 처리를 한 후 테스트를 진행하였습니다.
@WebMvcTest 어노테이션은 주로 API 스펙에 대해 테스트 혹은 Spring Rest Docs를 통해 API를 문서화할 때 주로 사용하고 있습니다.
테스트를 진행하는 동안 JPA 관련 Bean들만 생성합니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@OverrideAutoConfiguration(
enabled = false
)
@TypeExcludeFilters({DataJpaTypeExcludeFilter.class})
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
...
}
위 어노테이션은 JpaRepository.class interface 관련 Bean만 등록해주기 때문에 JpaRepository.class 를 사용하지 않은 Querydsl 관련 Repostiory를 테스트하기 적합하지 않습니다.
저는 Querydsl Repository를 함께 테스트하기 위해 @DataQuerydslTest를 만들어주었습니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repository
public @interface QuerydslRepository {
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest(properties = {
"spring.jpa.properties.hibernate.format_sql=true"
})
@ComponentScan(
useDefaultFilters = false,
includeFilters = {
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = { QuerydslRepository.class }
)
},
basePackageClasses = baseClass...
)
public @interface DataQuerydslTest {
}
@DataQuerydslTest
class PostRepositoryTest {
@Autowired
PostRepository postRepository;
@Test
void 게시글_단건_조회(){
// given
Post post = Post.of("title", "content", "writer");
post = postRepository.save(post);
// when
PostResource postResource = postRepository.findById(post.getId()).get();
// then
assertNotNull(postResource);
}
}
테스트시 @DataJpaTest, @WebMvcTest로 인해 Bean으로 등록되지 않지만, 특정 Bean들을 등록하고 싶을때 사용하는 어노테이션입니다.
@DataQuerydslTest
@Import({PostService.class, PostMapper.class})
class PostServiceTest_5 {
@Autowired
PostService postService;
@Autowired
PostRepository postRepository;
@Test
void 게시글_생성() {
// when
WritePostDto writePostDto = WritePostDto.of("title", "content");
Long postId = postService.write(writePostDto, "writer");
// then
assertNotNull(postId);
}
}
위 어노테이션을 사용해 PostService와 그 안에 속해있는 PostMapper Bean을 등록한 것을 볼 수 있습니다.
주로 전체적인 로직 테스트를 위해 h2 데이터베이스를 띄우고 해당 서비스 로직을 테스트 할때 사용하고 있습니다.