토이 프로젝트 스터디 #12
- 스터디 진행 날짜 : 6/23
- 스터디 작업 날짜 : 6/21 ~ 6/23
토이 프로젝트 진행 사항
- 카테고리 응답
DTO
계층화 적용
- 테스트 코드 작성
내용
NestedConvertUtils
public class NestedConvertUtils<K, E, D> {
private List<E> entities;
private Function<E, D> toDto;
private Function<E, E> getParent;
private Function<E, K> getKey;
private Function<D, List<D>> getChildren;
public static <K, E, D> NestedConvertUtils newInstance(List<E> entities, Function<E, D> toDto, Function<E, E> getParent, Function<E, K> getKey, Function<D, List<D>> getChildren) {
return new NestedConvertUtils<K, E, D>(entities, toDto, getParent, getKey, getChildren);
}
private NestedConvertUtils(List<E> entities, Function<E, D> toDto, Function<E, E> getParent, Function<E, K> getKey, Function<D, List<D>> getChildren) {
this.entities = entities;
this.toDto = toDto;
this.getParent = getParent;
this.getKey = getKey;
this.getChildren = getChildren;
}
public List<D> convert() {
try {
return convertInternal();
} catch (NullPointerException e) {
throw new CannotConvertNestedStructureException(e);
}
}
private List<D> convertInternal() {
Map<K, D> map = new HashMap<>();
List<D> roots = new ArrayList<>();
for (E e : entities) {
D dto = toDto(e);
map.put(getKey(e), dto);
if (hasParent(e)) {
E parent = getParent(e);
K parentKey = getKey(parent);
D parentDto = map.get(parentKey);
getChildren(parentDto).add(dto);
} else {
roots.add(dto);
}
}
return roots;
}
private List<D> getChildren(D d) {
return getChildren.apply(d);
}
private E getParent(E e) {
return getParent.apply(e);
}
private boolean hasParent(E e) {
return getParent(e) != null;
}
private K getKey(E e) {
return getKey.apply(e);
}
private D toDto(E e) {
return toDto.apply(e);
}
}
- 다른
Entity
도 적용할 수 있도록 Generic
활용
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class NestedCategoryResponse {
private Long id;
private String name;
private long depth;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<NestedCategoryResponse> children;
public static List<NestedCategoryResponse> fromEntity(List<Category> categories) {
NestedConvertUtils utils = NestedConvertUtils.newInstance(
categories,
c -> new NestedCategoryResponse(c.getId(), c.getName(), c.getDepth(), new ArrayList<>()),
c -> c.getParent(),
c -> c.getId(),
d -> d.getChildren()
);
return utils.convert();
}
}
- 위와 같이
DTO
변환 시에 NestedConvertUtils
활용
테스트 코드
카테고리 계층화 테스트
@ExtendWith(MockitoExtension.class)
@DisplayName("CategoryController 테스트")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class CategoryControllerTest {
@Mock
CategoryService categoryService;
@InjectMocks
CategoryController categoryController;
ObjectMapper objectMapper = new ObjectMapper();
MockMvc mockMvc;
@BeforeEach
void beforeEach() {
mockMvc = MockMvcBuilders
.standaloneSetup(categoryController)
.alwaysDo(print())
.build();
}
@Test
void findAllCategories_테스트() throws Exception {
Category category1 = CategoryEntityFactory.createCategory("a");
Category category2 = CategoryEntityFactory.createCategory("b");
Category category3 = CategoryEntityFactory.createCategoryWithParent("c", category1);
setField(category1, "id", 1L);
setField(category2, "id", 2L);
setField(category3, "id", 3L);
List<NestedCategoryResponse> response = CategoryDtoFactory.createFindAllCategoriesDto(List.of(category1, category2, category3));
given(categoryService.findAll()).willReturn(response);
ResultActions result = mockMvc.perform(
get("/api/categories")
);
result
.andExpect(status().isOk())
.andExpect(jsonPath("$.categories[0].id").value(1))
.andExpect(jsonPath("$.categories[0].name").value("a"))
.andExpect(jsonPath("$.categories[0].depth").value(0))
.andExpect(jsonPath("$.categories[0].children[0].id").value(3))
.andExpect(jsonPath("$.categories[0].children[0].name").value("c"))
.andExpect(jsonPath("$.categories[0].children[0].depth").value(1))
.andExpect(jsonPath("$.categories[1].id").value(2))
.andExpect(jsonPath("$.categories[1].name").value("b"))
.andExpect(jsonPath("$.categories[1].depth").value(0));
}
@Test
void addCategory_테스트() throws Exception {
CreateCategoryRequest request = CategoryDtoFactory.createCategoryRequestDto("a", null);
willDoNothing().given(categoryService).save(any(CreateCategoryRequest.class));
ResultActions result = mockMvc.perform(
post("/api/categories")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
result
.andExpect(status().isCreated());
verify(categoryService).save(any(CreateCategoryRequest.class));
}
@Test
void deleteCategory_테스트() throws Exception {
willDoNothing().given(categoryService).delete(anyLong());
ResultActions result = mockMvc.perform(
delete("/api/categories/{id}", 1L)
);
result
.andExpect(status().isOk());
verify(categoryService).delete(anyLong());
}
}
- 위와 같이 카테고리
DTO
반환 시 계층형으로 출려되는지 테스트 적용
CategoryService Lambda 테스트 검증 누락
@Test
void save_성공_테스트() {
CreateCategoryRequest request = CategoryDtoFactory.createCategoryRequestDto("a", null);
Category category = CategoryEntityFactory.createCategory("a");
given(categoryRepository.save(any(Category.class))).willReturn(category);
categoryService.save(request);
verify(categoryRepository).save(any(Category.class));
}
@Test
void save_부모_category_조회_실패_테스트() {
CreateCategoryRequest request = CategoryDtoFactory.createCategoryRequestDto("a", 2L);
given(categoryRepository.findById(anyLong())).willThrow(CategoryNotFoundException.class);
assertThrows(CategoryNotFoundException.class, () -> categoryService.save(request));
}
Lambda
로 작성한 코드 중 정상적으로 테스트 하지 못한 부분 발견
- 부모가 없는 카테고리 테스트만 진행했으므로 부모가 있는 카테고리 테스트 필요
@Test
void save_parent_테스트() {
Category parent = CategoryEntityFactory.createCategory("a");
categoryRepository.save(parent);
Category category = CategoryEntityFactory.createCategoryWithParent("b", parent);
Category saveCategory = categoryRepository.save(category);
assertEquals(category.getName(), saveCategory.getName());
assertEquals(category.getParent().getName(), saveCategory.getParent().getName());
}
UnnecessaryStubbingException
@ParameterizedTest
@CsvSource(
value = {
":",
"''"
},
delimiter = ':'
)
void addCategory_createCategoryRequest_name_notblank_검증_실패_테스트(String name) throws Exception {
CreateCategoryRequest request = CategoryDtoFactory.createCategoryRequestDto(name, null);
willDoNothing().given(categoryService).save(any(CreateCategoryRequest.class));
ResultActions result = mockMvc.perform(
post("/api/categories")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
result
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(1003))
.andExpect(jsonPath("$.message").value("이름을 입력해주세요."));
}
- 위와 같이 테스트 코드를 작성했을 때 동작 결과는 다음과 같이
UnnecessaryStubbingException
발생
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at com.project.board.application.category.controller.CategoryControllerAdviceTest.addCategory_createCategoryRequest_name_notblank_검증_실패_테스트(CategoryControllerAdviceTest.java:84)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
willDoNothing()
을 제거하니 정상적으로 동작
- 해당 테스트의 경우
Service
계층으로 가기 전 Controller
에서 @Validated
에 의해 바로 예외를 던지게 됨
Controller -> @Validated -> RestControllerAdvice
로 동작
Service
계층의 코드가 동작하지 않기 때문에 무의미한 Stubbing
이였고, 이를 Mockito
가 감지한 것
- 테스트 코드를 작성할 때 어느 계층의 코드가 동작하는지 정확히 판단해야 한다고 느낌
카테고리 테스트 진행도