inflearn 강의: Practical Testing: 실용적인 테스트 가이드를 공부하고, 비슷한 예시를 만들어 정리한 글입니다.
테스트 코드를 짤 때 외부 외존성으로 인해 테스트 하기 난해한 부분들이 있다.
해당 코드를 비즈니스 로직에서 제외하면 테스트를 좀 더 수월하게 처리할 수 있고, 비즈니스 로직에 집중할 수 있다.
그런 상황을 정의하고, 예시로 알아보도록 하자.
- 관측할 때마다 다른 값에 의존하는 코드 : 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력
- 외부 세계에 영향을 주는 코드 : 표준 출력(로그), 메시지 발송(이메일), 데이터베이스에 기록(또는 파일에 기록)
순수 함수(pure function)
- 같은 입력에는 항상 같은 결과가 나오는 함수(input, output이 명확한 메서드)
- 외부 세상과 단절된 형태 (utility function)
@Getter
@RequiredArgsConstructor(staticName = "of")
public class User {
private final String userId;
}
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Apply {
private final User user;
private final LocalDateTime appliedTime;
}
@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가 항상 통과 가능하도록한다.