테스트하기 어려운 영역을 구분하고, 분리하기

weightle55·2024년 1월 6일
0

TIL

목록 보기
1/2

inflearn 강의: Practical Testing: 실용적인 테스트 가이드를 공부하고, 비슷한 예시를 만들어 정리한 글입니다.

들어가기

테스트 코드를 짤 때 외부 외존성으로 인해 테스트 하기 난해한 부분들이 있다.
해당 코드를 비즈니스 로직에서 제외하면 테스트를 좀 더 수월하게 처리할 수 있고, 비즈니스 로직에 집중할 수 있다.
그런 상황을 정의하고, 예시로 알아보도록 하자.


테스트 하기 어려운 영역

  • 관측할 때마다 다른 값에 의존하는 코드 : 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력
  • 외부 세계에 영향을 주는 코드 : 표준 출력(로그), 메시지 발송(이메일), 데이터베이스에 기록(또는 파일에 기록)

반대로 테스트하기 좋은 함수

순수 함수(pure function)

  • 같은 입력에는 항상 같은 결과가 나오는 함수(input, output이 명확한 메서드)
  • 외부 세상과 단절된 형태 (utility function)

도메인 설정

  • 특정 시간에만 응모할 수 있는 이벤트
  • 유저
  • 이벤트
  • 응모

User(유저)

  • 이벤트에 응모하는 유저
  • fields
    * 식별자 ID
@Getter
@RequiredArgsConstructor(staticName = "of")
public class User {
    private final String userId;
}

Apply(응모)

  • 유저가 이벤트에 응모
  • fields
    유저객체
    응모 시간
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Apply {
    private final User user;
    private final LocalDateTime appliedTime;
}

Event(이벤트)

  • 유저가 응모한 이벤트
  • 응모는 오전 11에서 오후 9시까지만 진행된다.
  • fields
    이벤트 식별자 : eventName
    이벤트 응모가능 시작시간 : startTime
    * 이벤트 응모가능 종료시간 : endTime
    • 해당 이벤트의 유저 응모 List : userApplies
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Event {
    private final String eventName;
    private final LocalDateTime startTime;
    private final LocalDateTime endTime;
    private final List<Apply> userApplies = new ArrayList<>();

    /**
     * Apply 객체를 userApplies에 추가한다.
     * @param apply
     */
    public void userApply(Apply apply) {
        userApplies.add(apply);
    }

    /**
     * User를 받아 지원을 생성한다.
     * @param user
     */
    public void userApply(User user) {
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(startTime) && now.isAfter(endTime)) {
            throw new IllegalArgumentException("지원 가능한 시간이 아닙니다.");
        }
        Apply apply = Apply.of(String.format("%s%s", now.toString(), user.getUserId()), user, now);
        userApply(apply);
    }
}

유저 응모 테스트

event userApply(User user)를 테스트 한다.

class EventTest {
...
    @DisplayName("지원가능한 시간(AM 11:00:00 ~ PM 09:00:00)에 이벤트에 응모하여, 정상 응모 된다.")
    @Test
    public void userApplyTestByUser() {
        //given
        Event event = Event.of("테스트이벤트",
                LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0, 0)),
                LocalDateTime.of(LocalDate.now(), LocalTime.of(21, 0, 0))
        );
        User user = User.of("testUser");

        //when
        event.userApply(user);

        //then
        Assertions.assertThat(event.getUserApplies().size()).isEqualTo(1);
        Assertions.assertThat(event.getUserApplies().get(0).getUser().getUserId()).isEqualTo("testUser");
        Assertions.assertThat(event.getUserApplies().get(0).getAppliedTime()).isEqualTo(LocalDateTime.of(LocalDate.now(), LocalTime.now()));
    }
}

결과

org.opentest4j.AssertionFailedError: 
expected: 2024-01-06T19:54:01.686966800 (java.time.LocalDateTime)
 but was: 2024-01-06T19:54:01.663414700 (java.time.LocalDateTime)
when comparing values using 'ChronoLocalDateTime.timeLineOrder()'
필요:2024-01-06T19:54:01.686966800 (java.time.LocalDateTime)
실제   :2024-01-06T19:54:01.663414700 (java.time.LocalDateTime)

event.userApply(user)가 실행되는 now와 isEqualTo가 실행될 때 실행되는 now가 다르기 때문에 실패한다.

해당 값 appliedTime을 reflection을 이용해 외부에서 값을 세팅해서 테스트도 가능하나, 그렇게 하더라도, 테스트 Time이 오전 11시 ~ 오후 9시가 아닌 시간에 실행될 경우 실패하게 된다.

이 부분은 관측할 때마다 다른 값에 의존하는 코드로 해당 부분을 Parameter로 전환해서 테스트의 신뢰성을 높인다.

@Getter
@RequiredArgsConstructor(staticName = "of")
public class Event {
    ...

    /**
     * User를 받아 지원을 생성한다.
     * @param user
     */
    public void userApply(User user, LocalDateTime now) {
//        LocalDateTime now = LocalDateTime.now(); --> now를 Parameter로 변환
        if (now.isBefore(startTime) && now.isAfter(endTime)) {
            throw new IllegalArgumentException("지원 가능한 시간이 아닙니다.");
        }
        Apply apply = Apply.of(String.format("%s%s", now.toString(), user.getUserId()), user, now);
        userApply(apply);
    }
}
class EventTest {

    @DisplayName("지원가능한 시간(AM 11:00:00 ~ PM 09:00:00)에 이벤트에 응모하여, 정상 응모 된다.")
    @Test
    public void userApplyTestByUser() {
        //given
        Event event = Event.of("테스트이벤트",
                LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0, 0)),
                LocalDateTime.of(LocalDate.now(), LocalTime.of(14, 0, 0))
        );
        User user = User.of("testUser");
        //now를 로직 외부에서 설정
        LocalDateTime now = LocalDateTime.now();
        
        //when
        event.userApply(user, now);

        //then
        Assertions.assertThat(event.getUserApplies().size()).isEqualTo(1);
        Assertions.assertThat(event.getUserApplies().get(0).getUser().getUserId()).isEqualTo("testUser");
        //Assertions.assertThat(event.getUserApplies().get(0).getAppliedTime()).isEqualTo(LocalDateTime.of(LocalDate.now(), LocalTime.now()));
        Assertions.assertThat(event.getUserApplies().get(0).getAppliedTime()).isEqualTo(now);
    }
}

지원 시간 appliedTime을 외부에서 설정하여, Test가 항상 통과 가능하도록한다.

0개의 댓글