2jigoo / BookStudy-StartTdd

'테스트 주도 개발 시작하기' 스터디
2 stars 0 forks source link

[7주차-1] 테스트 코드와 유지보수 (1/2) - jongha #28

Closed Jonghai closed 1 year ago

Jonghai commented 1 year ago

Chapter 10. 테스트 코드와 유지보수 (1/2)

스터디 일시 2023.09.14

목표

Jonghai commented 1 year ago

Chapter.10 테스트 코드와 유지보수

테스트 코드와 유지보수

테스트 코드는 그 자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상이 된다.

테스트를 방치하는 상황이 길어지면 테스트 코드는 가치를 잃기 시작한다.
이런 악순환이 발생하지 않으려면 테스트 코드 자체의 유지보수성이 좋아야 한다.

테스트 코드 만들 때 주의 사항

변수나 필드를 사용해서 기댓값 표현하지 않기

    @Test
    void dateFormat(){
    LocalDate date = LocalDate.of(1945,8,15);
    String dateStr = formatDate(date);
    assertEquals(date.getYear() + "년" +
            date.getMonthValue() + "월" +
            date.getDayOfMonth() + "일", dateStr);
    }
    @Test
    void dateFormat(){
    LocalDate date = LocalDate.of(1945,8,15);
    String dateStr = formatDate(date);
    assertEquals("1945년 8월 15일", dateStr);
    }
private List<Integer> answers = Arrays.asList(1,2,3,4);
    private Long respondentId = 100L;

    @DisplayName("답변에 성공하면 결과 저장함")
    @Test
    public void saveAnswerSuccessfully(){
        //답변할 설문이 존재
        Survey survey = SurveyFactory.createApprovedSurvey(1L);
        surveyRepository.save(survey);

        //설문 답변
        SurveyAnswerRequest surveyAnswer = SurveyAnswerRequest.builder()
                .surveyId(survey.getId())
                .reapondentId(respondentId)
                .answers(answers)
                .build();

        svc.answerSuvey(surveyAnswer);

        //저장 결과 확인
        SurverAnswer savedAnswer =
                memoryRepository.findBySurveyAndRespondent(
                        survey.getId(),respondentId);
        assertAll(
                () -> assertEquals(respondentId, savedAnswer.getRespondentId()),
                () -> assertEquals(answers.size(), savedAnswer.getAnswer().size()),
                () -> assertEquals(answers.get(0), savedAnswer.getAnswer().get(0)),
                () -> assertEquals(answers.get(1), savedAnswer.getAnswer().get(1)),
                () -> assertEquals(answers.get(2), savedAnswer.getAnswer().get(2)),
                () -> assertEquals(answers.get(2), savedAnswer.getAnswer().get(3))
        );
    }


=> 객체 생성 시 변수와 필드 값 대신 값 자체를 사용하여 코드 수정.

SurveyAnswerRequest surveyAnswer = SurveyAnswerRequest.builder()
                .surveyId(1L)
                .reapondentId(100L)
                .answers(Arrays.asList(1,2,3,4))
                .build();

        svc.answerSuvey(surveyAnswer);
SurverAnswer savedAnswer =
                memoryRepository.findBySurveyAndRespondent(
                        1L, 100L);
        assertAll(
                () -> assertEquals(100L, savedAnswer.getRespondentId()),
                () -> assertEquals(4, savedAnswer.getAnswer().size()),
                () -> assertEquals(1, savedAnswer.getAnswer().get(0)),
                () -> assertEquals(2, savedAnswer.getAnswer().get(1)),
                () -> assertEquals(3, savedAnswer.getAnswer().get(2)),
                () -> assertEquals(4, savedAnswer.getAnswer().get(3))
        );
Jonghai commented 1 year ago

두 개 이상을 검증하지 않기

  1. 회원가입 이후에 데이터가 올바르게 저장되는지 검증.
  2. 이메일 발송을 올바르게 요청하는지 검증.

한 테스트에서 검증하는 내용이 두 개 이상이면 테스트 결과를 확인 할 때 집중도가 떨어진다.

첫번째 검증 대상을 통과시켜야 비로소 두 번째 검증이 성공했는지 여부를 확인할 수 있다.
또한, 테스트에 실패했을 때 두 가지 검증 대상 중 무엇이 실패했는지 확인해야 한다.

    @DisplayName("같은 ID가 없으면 가입 성공함")
    @Test
    void noDupId_RegisterSuccess(){
        userRegister.register("id", "pw", "email");

        User savedUser = fakeRepository.finById("id");
        assertEquals("email", savedUser.getEmail());
    }

    @DisplayName("가입하면 메일을 전송함")
    @Test
    void whenRegisterThenSendMail(){
        userRegister.register("id", "pw", "email@email.com");

        Advice.ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        then(mockEmailNotifier).should().sendRegisterEmail(captor.capture());

        String realEmail = captor.getValue();
        assertEquals("email@email.com", realEmail);
    }
Jonghai commented 1 year ago

정확하게 일치하는 값으로 모의 객체 설정하지 않기

예) 약한 암호면 가입 실패

@DisplayName("약한 암호면 가입 싶패")
    @Test
    void weakPassword(){
        BDDMockito
                .given(mockPasswordChecker.checkPasswordWeak("pw"))
                .willReturn(true);

        assertThrows(WeakPasswordException.class, ()->{
            userRegister.register("id","pw","email");
        });
    }

만약 더음과 같이 코드르 변경했을 때 false를 리턴하여 테스트 실패.

userRegister.register("id","pwa","email");

모의 객체는 "pw"가 아니라 임의의 문자열에 대해 true를 리턴해도 테스트의 의도에 전혀 문제가 되지 않는다.

    @DisplayName("약한 암호면 가입 싶패")
    @Test
    void weakPassword(){
        BDDMockito
                .given(mockPasswordChecker.checkPasswordWeak(Mockito.anyString()))
                .willReturn(true);

        assertThrows(WeakPasswordException.class, ()->{
            userRegister.register("id","pw","email");
        });
    }

Mockito.anyString()은 임의의 String 값에 일치한다는 것을 의미.
즉, 모의 객체가 임의의 String 값에 대해 true를 리턴하도록 설정.

이제 "pw"가 아닌 다른 문자열을 인자로 전달해도 테스트는 깨지지 않는다.

Jonghai commented 1 year ago

과도하게 구현 검증하지 않기

내부 구현을 검증하는 것이 나쁜 것은 아니지만 한가지 단점이 있다.

조금만 변경해도 테스트가 깨질 가능성이 커진다.

예)
중복ID가 존재하는지 먼저 확인하고 그 다음에 아이디가 약한지 검사하도록 register() 메서드의 구현을 변경한다고 할 때, 테스트는 깨진다.

=> 중복 아이디를 검사하는 과정에서 UserRepository의 findById() 메서드를 호출하기 때문이다.

내부 구현은 언제든지 바뀔 수 있기 떄문에 테스트 코드는 내부 구현보다 실행 결과를 검증해야 한다.

하지만 레거시 코드같은 경우 어쩔 수 없이 내부 구현을 검증해야 할 때도 있다.

셋업을 이용해서 중복된 상황을 설정하지 않기

테스트 코드에서 동일한 상황으로 중복된 코드를 제거하기 위해 @BeforeEach 메서드를 이용해서 상황을 구성할 수 있다.

중복을 제거하고 코드 길이도 짧아져서 코드 품질이 좋아졌다고 생각할 수 있지만, 테스트 코드에서는 상황이 달라진다.

몇달만에 다시 테스트 코드를 봤을 때 테스트에 실패한 이유를 알려면 어떤 상황인지 분석해야한다.
기억이 잘 나지 않는 코드를 위 아래로 이동하면서 실패 원인을 분석해야한다.

또한

모든 메서드가 동일한 상황 코드를 공유하기 떄문에 조금만 내용을 변경해도 테스트가 깨질 수 있다.

=> 테스트 메서드는 검증을 목표로 하는 하나의 완전한 프로그램이어야 한다.
그러기 위해서는 상황 구성 코드가 테스트 메서드 안에 위치해야한다.