Test는 실제 객체를 사용을 해서 Test를 하는 방법도 있지만 가짜 객체를 만들어서 Test를 하는 방법도 있다. 이번 포스트에서는 가짜 객체를 만들어서 Test를 하는 방법에 대해서 알아볼 것이다.
보통 가짜 객체를 만들어서 테스트를 하는 방법을 Mock이라고 한다.
코드를 살펴보기 전에 Mock이 무엇인지에 대해서 알아보자.
Mock을 우리나라 말로 변역을 해보면 명사로는 모조품, 동사로는 속이다 라는 의미가 있다.
이를 테스트에 적용을 시켜보면 모조품 객체를 만들어서 테스트를 한다 라고 생각을 할 수 있다.
모조품 객체를 Mock 객체 혹은 Mock Object라고 한다.
이 Mock 객체은 테스트를 하고자 하는 코드에서 사용이 되는 외부 의존성을 제거하기 위해서 사용이 된다.
의존성을 제거하면 테스트를 하고자하는 코드에만 집중을 할 수 있게 된다.
이는 테스트의 정확성과 효율성이 증가하는데 도움이 된다.
Spring Framwork에서 사용되는 Mock 라이브러리로 Mockito가 있다.
이 Mockito를 사용을 하면 간단한 어노테이션으로 외부의 의존성을 모킹을 할 수 있다.
테스트에도 여러가지 종류의 테스트가 있다.
단위 테스트, 통합 테스트 등 여럭 단계에서 테스트를 사용을 할 수 있는데 Mock은 단위 테스트를 작성을 할 때 사용이 된다.
단위 테스트는 작은 단위 코드를 테스트하기 때문에 빠르고 가겹게 진행이 되어야 한다.
실제 객체들을 사용하게 되면 Spring Application Context를 전부 올리게 되어서 속도가 늦는 경향이 있고 무거워진다.
그래서 Mock 객체를 사용을 하면 가볍고 빠르게 사용이 가능하기 때문에 MocK을 많이 사용을 하는 경향이 있다.
@Mock
private CategoryRepository categoryRepository;
@InjectMocks
private CategoryService categoryService;
@DisplayName("카테고리의 level을 넣으면, level에 해당하는 데이터들의 리스트가 나온다.")
@Test
void getCategoriesByParentCategoryId() {
// given
CategoryEntity categoryEntity2 = CategoryEntity.builder()
.categoryName("축구")
.categoryLevel(2)
.parentCategoryId(1L)
.build();
CategoryEntity categoryEntity3 = CategoryEntity.builder()
.categoryName("야구")
.categoryLevel(2)
.parentCategoryId(1L)
.build();
BDDMockito.given(categoryRepository.findAllByParentCategoryId(1L))
.willReturn(List.of(categoryEntity2, categoryEntity3));
// when
List<CategoryResponseDTO> categories = categoryService.getCategoryByParentCategoryId(1L);
// then
assertThat(categories).hasSize(2)
.extracting("categoryName", "categoryLevel", "parentCategoryId")
.containsExactlyInAnyOrder(
tuple("축구", 2, 1L),
tuple("야구", 2, 1L));
}
Mock을 살펴보기 위해서 내가 진행 중인 카테고리의 level로 카테고리를 찾는 코드를 테스트하는 코드를 가지고 왔다.
코드를 살펴보면 given, when, then이 있다.
given은 주어진 상황을 의미한다. 테스트를 진행을 하기 전에 초기 상태나 조건을 설정하는 것이다.
when은 어떤 행동을 했을 때를 의미를 한다. 즉, categoryService에서 findAllByParentCategoryId라는 메소드를 실행하는 것이다.
then은 어떤 결과가 나와야 하는 지를 검증하는 단계이다. findAllByParentCategoryId라는 메소드가 실행이 되었을 때 어떤 결과가 나와야 하는데 그 결과가 나왔는 지를 확인하는 부분이다.
Mock을 사용할 때, 수동으로 넣어줄 수도 있지만 나는 어노테이션을 사용을 해서 자동으로 넣어주었다.
@Mock과 @InjectMocks 사용할 수 있는데 이는 CategoryService의 일부분 코드를 보면서 설명을 하겠다.
private final CategoryRepository categoryRepository;
public List<CategoryResponseDTO> getCategoryByCategoryLevel(int categoryLevel) {
List<CategoryResponseDTO> categories = new ArrayList<>();
categoryRepository.findAllByCategoryLevel(categoryLevel).forEach(categoryEntity -> {
categories.add(new CategoryResponseDTO(categoryEntity));
});
return categories;
}
CategoryService에 보면 CategoryRepository를 주입을 받고 있다.
이렇게 주입을 해줘야 하는 객체에는 @Mock을 그리고 @Mock이 달린 객체를 주입 받아야 하는 객체에는 @InjectMocks를 달아주어야 한다.
그러면 자동으로 의존성 주입을 해준다.
그리고 CategoryRepository에서 findAllByCategoryLevel이라는 메소드를 통해서 카테고리들을 불러오고 있다.
이 부분을 명시적으로 넣어주어야 한다. 즉, 해당 메소드의 반환 값을 임의로 지정을 해주어야 한다.
BDDMockito.given(categoryRepository.findAllByParentCategoryId(1L))
.willReturn(List.of(categoryEntity2, categoryEntity3));
이 부분이다.
이러한 과정을 Stub라고 하는데, 이 Stub를 해주어야 하는 이유는 뭘까?
여기서 categoryRepository는 @Mock으로 만든 가짜 객체, Mock 객체이다.
즉, 가짜이기 때문에 findAllByParentCategoryId 메소드를 실제 실행을 할 수가 없다.
그래서 우리가 Mock 객체 categoryRepository는에서 findAllByParentCategoryId 메소드를 실행을 헸을 떄, 어떤 결과가 나올 것이라는 결과를 명시적으로 지정을 해주어야 한다.
이렇게 주어진 상황을 주고 when에서 우리가 테스트를 하고자 하는 메소드를 실제로 실행을 한다.
assertThat을 통해서 우리가 얻은 결과에서 어떤 부분을 검증을 하고 싶은지 체크를 하면 된다.
테스트를 하는 방법은 @SpringBootTest, @DataJpaTest, @Mock 등 다양한 방법이 있다.
방법들 중 어떤 것이 정답이라는 것은 없다.
단지 지금 내 상황에서 어떤 것이 더 효율적인지 판단을 해서 사용하는 것이 중요하다.
이번 포스트에서는 @Mock에 대해서 알아보았다.
Mock은 내가 집중적으로 테스트를 하고자하는 코드의 외부 주입을 분리 시키기 위해서 가짜 객체를 사용하는 것이다.
주로 작은 단위의 단위 테스트에서 사용하는 것을 권장 한다.