-
타임머신 테스트 하기🕰(feat.LocalDateTime.now(clock))카테고리 없음 2022. 8. 7. 20:56
0. Intro
@Entity @EntityListeners(AuditingEntityListener.class) public class AuthCode { private static final long VALID_MINUTE = 5L; @CreatedDate private LocalDateTime createdAt; //... public void verifyTime() { LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE); if (LocalDateTime.now().isAfter(expireTime)) { throw new InvalidAuthCodeException(); } } }
인증코드가
생성시점에서 5분이 지나면 만료
되는 로직을 짜던 도중이었다.구현 후 테스트코드를 짜려고 보니 문제가 생겼다.
타임머신을 타고 싶다 LocalDateTime.now()
는 시스템의 지금 시간 을 리턴하는데어떻게
특정 시간으로부터 만료되었는지
검증하는 테스트 코드를 짤 수 있지? (어떻게 타임머신을 타지??)즉, 시간 변수를 테스트에서 임의로 지정해 쓸 수 없었다.
그래서
현재시각을 임의지정 후 모킹
해 테스트하는 방법을 생각해봤다.
1. 현재 시각을 모킹하는 법
여러가지 방법이 있었다.
1. Clock 을 빈 등록해 모킹하기
2. LocalDateTime 을 static mock으로 모킹하기
3. LocalDateTime.now() 를 리턴하는 새 클래스 생성해 모킹하기
4. AuthCode 생성자에서 LocalDateTime 인터페이스를 받고, 그 안에서 LocalDateTime/mock을 넣기여러 방법들 중 나는 1번,
Clock
을 빈 등록해 모킹하기 를 택했다.왜냐하면
2번 static mock은 테스트코드 작성 방식이 복잡하고 try문으로 감싸야 한다.
3번은 now()이외의 시간변수를 모킹하고 싶을 때마다 일일히 지정해줘야 한다.
4번은 어디까지 해당 객체를 넣어줄지 범위를 끊어주기 어려웠다.(Controller →service→domain…)
2. 왜
Clock
인가?1)
LocalDateTime
은 모킹할 수 없다public final class LocalDateTime implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable { public static LocalDateTime now() { return now(Clock.systemDefaultZone()); } public static LocalDateTime now(ZoneId zone) { return now(Clock.system(zone)); } public static LocalDateTime now(Clock clock) { Objects.requireNonNull(clock, "clock"); final Instant now = clock.instant(); // called once ZoneOffset offset = clock.getZone().getRules().getOffset(now); return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset); } //... of() 등 이외 메소드 }
LocalDateTime.now()
는 다음 방식으로 짜여있다.보다시피 now()는 static 메소드이기 때문에
LocalDateTime을 객체로 등록해 사용하더라도 메소드를 꺼내쓸 수 없다…ㅠㅠ그런데 여기서 주목할 점이 있다.
public static LocalDateTime now(Clock clock) { Objects.requireNonNull(clock, "clock"); final Instant now = clock.instant(); // called once ZoneOffset offset = clock.getZone().getRules().getOffset(now); return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset); }
평소에는
LocalDateTime.now()
로 현재 시간을 가져오지만,사실
LocalDateTime.now(clock)
형태로,Clock
객체를 파라미터로 받아 시간을 가져오는 형태였다!2)
Clock
이 뭐지?그렇다면 이
Clock
은 뭘까?정확히 말하면
final Instant now = clock.instant(); // called once
에서 사용되는
clock.instant()
는 뭘까?public abstract class Clock { //----------------------------------------------------------------------- /** * Gets the current instant of the clock. *<p> * This returns an instant representing the current instant as defined by the clock. * *@returnthe current instant from this clock, not null *@throwsDateTimeExceptionif the instant cannot be obtained, not thrown by most implementations */ public abstract Instant instant(); //... }
Clock 객체의 현재 instant를 리턴해주는 추상 메소드이다.
3)
Clock
을 빈으로 등록하자즉,
LocalDateTime
의 메소드들은static
메소드지만,clock
객체를 파라미터로 받을 수 있으니clock
을 빈으로 등록해 사용하면 된다!3. 어떻게 모킹하지?
1) 현재 시간(
now
)을 파라미터로 추출public class AuthCode { public void verifyTime(LocalDateTime now) { // now를 파라미터로 이동 LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE); if (now.isAfter(expireTime)) { throw new InvalidAuthCodeException(); } } }
authcode.verifyTime()
메소드에서현재 시간(
now
)을 파라미터로 추출했다.2)
clock
을Bean
등록import java.time.Clock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class TimeConfig { @Bean public Clock clock() { return Clock.systemDefaultZone(); } }
다음,
clock
을@Configuration
으로 빈 등록했다.public abstract class Clock { public static Clock systemDefaultZone() { return new SystemClock(ZoneId.systemDefault()); } }
이때
Clock.systemDefaultZone()
을 활용했다.clock
클래스의 현재시간clock
객체를 리턴한다.3)
AuthService
에서Bean
주입그리고
AuthService
에서clock
을 빈으로 주입받은 후AuthCode
객체에clock
을 주입한다.@Service public class AuthService { private final AuthCodeRepository authCodeRepository; // ... private final Clock clock; public AuthService(//..., Clock clock) { this.authCodeRepository = authCodeRepository; this.clock = clock; } public void verifyAuthCode(VerificationRequest verificationRequest) { AuthCode authCode = authCodeRepository.findBySerialNumber(serialNumber) .orElseThrow(SerialNumberNotFoundException::new); //... LocalDateTime now = LocalDateTime.now(clock); authCode.verifyTime(now); } // ... }
4. 어떻게 테스트하지?
1)
AuthCode
(도메인 계층)AuthCode
의 경우 등록된clock
빈을 사용하지 않고도 다음 방식으로 테스트할 수 있다.@SpringBootTest class AuthCodeTest { @DisplayName("인증코드 생성시간으로부터 5분이 지나면 인증이 불가능하다") @Test void verifyTime_Exception_Time() { AuthCode authCode = AuthCode.builder() .code("ABCDEF") .serialNumber("21f46568bf6002c23843d198af30bb2bc8123695bd3d12ce86e0fc35bc5d3279") .createdAt(LocalDateTime.parse("2007-12-03T10:15:30")) .build(); assertThatThrownBy(() -> authCode.verifyTime(LocalDateTime.parse("2007-12-03T10:20:31"))) .isInstanceOf(InvalidAuthCodeException.class); } }
2)
AuthService
(서비스 계층)AuthService
의 경우clock
빈을 이용해 다음 방식으로 테스트할 수 있다.class AuthServiceTest extends IntegrationTest { private static final Clock FUTURE_CLOCK = Clock.fixed(Instant.parse("3333-08-22T10:00:00Z"), ZoneOffset.UTC); @Autowired private AuthService authService; @Autowired private AuthCodeRepository authCodeRepository; @SpyBean private Clock clock; @DisplayName("인증번호 만료 시 예외 발생") @Test void verifyAuthCode_Exception_Expired() { AuthCode authCode = //~ authCodeRepository.save(authCode); // clock.instant() 리턴값을 임의로 지정해 미래로 현재 시간을 바꾼다! doReturn(Instant.now(FUTURE_CLOCK)) .when(clock) .instant(); VerificationRequest verificationRequest = new VerificationRequest("test@gmail.com", "ABCDEF"); assertThatThrownBy(() -> authService.verifyAuthCode(verificationRequest)) .isInstanceOf(InvalidAuthCodeException.class); } }
📚참고자료
(Java) 타임머신을 타고 시간 여행 떠나기
문제의 발단가끔 현재 시간을 기준으로 코드를 짜야할 때가 있다.이런 경우에 자바의 경우에는 LocalDate, LocalTime, LocalDateTime 등등의 클래스에 있는 static 메서드인 now 메서드로 현재 시간을 구한다
perfectacle.github.io
테스트 코드에선 LocalDate.now()를 쓰지말자.
테스트 코드에선 LocalDate.now()를 쓰지말자.
여러 사람의 코드를 볼때 가끔 테스트 코드에서 LocalDate.now() 를 사용하는걸 종종 보게 됩니다. 아무래도 편하게 작성할 수 있다보니 사용된것 같지만, 이는 좋은 패턴이 아닙니다. 그래서 예제로
jojoldu.tistory.com
How can I mock java.time.LocalDate.now()
How can I mock java.time.LocalDate.now()
In my test case, I need test time sensitive method, in that method we're using java 8 class LocalDate, it is not Joda. What can I do to change time, when I'm running test
stackoverflow.com