클린 코드 (단위 테스트)

dasd412·2022년 7월 23일
0

단위 테스트에서 중요한 사항

단위 테스트에서는 가독성이 제일 중요하다.
실제 운영 환경에서는 효율성이 제일 중요하지만 테스트 환경에서는 해당 사항이 별로 중요하지 않다. 따라서 단위 테스트를 작성할 때는 효율적인 코드보다는 읽기 쉬운 코드를 작성하자.

작성 예시1

    @Transactional
    @Test
    public void saveWritersMany() {
        //given
        Writer me = saveDiaryService.saveWriter("ME", "ME@NAVER.COM", Role.User);
        Writer other = saveDiaryService.saveWriter("other", "OTHER@NAVER.COM", Role.User);
        Writer another = saveDiaryService.saveWriter("another", "Another@NAVER.COM", Role.User);

        //when
        Writer foundMe = writerRepository.findAll().get(0);
        Writer foundOther = writerRepository.findAll().get(1);
        Writer foundAnother = writerRepository.findAll().get(2);

        //then
        assertThat(foundMe).isEqualTo(me);
        assertThat(foundMe.getName()).isEqualTo(me.getName());
        assertThat(foundMe.getEmail()).isEqualTo(me.getEmail());
        assertThat(foundMe.getRole()).isEqualTo(me.getRole());
        logger.info(foundMe.toString());

        assertThat(foundOther).isEqualTo(other);
        assertThat(foundOther.getName()).isEqualTo(other.getName());
        assertThat(foundOther.getEmail()).isEqualTo(other.getEmail());
        assertThat(foundOther.getRole()).isEqualTo(other.getRole());
        logger.info(foundOther.toString());

        assertThat(foundAnother).isEqualTo(another);
        assertThat(foundAnother.getName()).isEqualTo(another.getName());
        assertThat(foundAnother.getEmail()).isEqualTo(another.getEmail());
        assertThat(foundAnother.getRole()).isEqualTo(another.getRole());
        logger.info(foundAnother.toString());
    }

도메인에 특화된 테스트 언어를 만들자.

잡다하고 세세한 사항으로 범벅된 코드를 좀 더 간결하고 표현력이 풍부한 코드로 리팩토링하자.

단위 테스트에는 한 개념만 테스트하라

이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피해야 한다.

리팩토링 결과

    @Transactional
    @Test
    public void saveWritersMany() {
        
        testSavingWriter("me", "me@NAVER.COM", Role.User, 0);
        
        testSavingWriter("other", "OTHER@NAVER.COM", Role.User, 1);

        testSavingWriter("another", "Another@NAVER.COM", Role.User, 2);
    }

    private void testSavingWriter(String name, String email, Role role, int savedSequence) {

        //given
        Writer writer = saveDiaryService.saveWriter(name, email, role);

        //when
        Writer found = writerRepository.findAll().get(savedSequence);

        //then
        assertThat(found).isEqualTo(writer);
        assertThat(found.getName()).isEqualTo(writer.getName());
        assertThat(found.getEmail()).isEqualTo(writer.getEmail());
        assertThat(found.getRole()).isEqualTo(found.getRole());
    }

testSavingWriter() 내 assert가 여러 개 있는 이유는 Writer의 equals() 기준이 id 뿐이기 때문이다.

작성 예시2

    @Test
    public void deleteDiary() throws Exception {
        //일지뿐만 아니라 연관된 식단, 음식도 전부 삭제하는 것이 목적이다.

        //given
        String url = "/api/diary/user/diabetes-diary";

        List<SecurityFoodDTO> breakFast = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("breakFast" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> lunch = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("lunch" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> dinner = IntStream.rangeClosed(1, 1).mapToObj(i -> new SecurityFoodDTO("dinner" + i, i))
                .collect(Collectors.toList());

        SecurityDiaryPostRequestDTO dto = SecurityDiaryPostRequestDTO.builder().fastingPlasmaGlucose(100).remark("test")
                .year("2021").month("12").day("22").hour("00").minute("00").second("00")
                .breakFastSugar(110).lunchSugar(120).dinnerSugar(130)
                .breakFastFoods(breakFast).lunchFoods(lunch).dinnerFoods(dinner).build();

        mockMvc.perform(post(url).with(user(principalDetails))
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value("true"))
                .andExpect(jsonPath("$.response.id").value(1));

        DiabetesDiary found = diaryRepository.findAll().get(0);


        //when and then
        String deleteUrl = "/api/diary/user/diabetes-diary/" + found.getId();
        mockMvc.perform(delete(deleteUrl).with(user(principalDetails)))
                .andDo(print());

        List<DiabetesDiary> diaries = diaryRepository.findAll();
        List<Diet> dietList = dietRepository.findAll();
        List<Food> foodList = foodRepository.findAll();

        assertThat(diaries.size()).isEqualTo(0);
        assertThat(dietList.size()).isEqualTo(0);
        assertThat(foodList.size()).isEqualTo(0);

    }

메서드의 추상화가 제대로 이루어져 있지 않아 가독성이 매우 나쁜 코드다.

리팩토링 결과

    @Test
    public void deleteDiary() throws Exception {
        //일지뿐만 아니라 연관된 식단, 음식도 전부 삭제하는 것이 목적이다.

        //given
        String url = "/api/diary/user/diabetes-diary";

        SecurityDiaryPostRequestDTO dto = makeDtoForDelete();

        postDiaryForDelete(url, dto);

        //when and then
        deleteRequest();

        deleteRequestIsValid();

    }

    private SecurityDiaryPostRequestDTO makeDtoForDelete() {
        List<SecurityFoodDTO> breakFast = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("breakFast" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> lunch = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("lunch" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> dinner = IntStream.rangeClosed(1, 1).mapToObj(i -> new SecurityFoodDTO("dinner" + i, i))
                .collect(Collectors.toList());

        return SecurityDiaryPostRequestDTO.builder().fastingPlasmaGlucose(100).remark("test")
                .year("2021").month("12").day("22").hour("00").minute("00").second("00")
                .breakFastSugar(110).lunchSugar(120).dinnerSugar(130)
                .breakFastFoods(breakFast).lunchFoods(lunch).dinnerFoods(dinner).build();
    }

    private void postDiaryForDelete(String url, SecurityDiaryPostRequestDTO dto) throws Exception {
        mockMvc.perform(post(url).with(user(principalDetails))
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value("true"))
                .andExpect(jsonPath("$.response.id").value(1));
    }

    private void deleteRequest() throws Exception {
        DiabetesDiary found = diaryRepository.findAll().get(0);

        String deleteUrl = "/api/diary/user/diabetes-diary/" + found.getId();
        mockMvc.perform(delete(deleteUrl).with(user(principalDetails)))
                .andDo(print());
    }

    private void deleteRequestIsValid() {
        List<DiabetesDiary> diaries = diaryRepository.findAll();
        List<Diet> dietList = dietRepository.findAll();
        List<Food> foodList = foodRepository.findAll();

        assertThat(diaries.size()).isEqualTo(0);
        assertThat(dietList.size()).isEqualTo(0);
        assertThat(foodList.size()).isEqualTo(0);
    }

@Test 적용중인 코드를 추상화했다. 그리고 가독성이 좋게 메서드 명을 구체적으로 적었다.
또한, 위에서 아래로 자연스럽게 읽히도록 메서드를 배치하였다.

profile
아키텍쳐 설계와 테스트 코드에 관심이 많음.

0개의 댓글