Updated:

1. 시간 조작 테스트 방법에 대한 고민

필자는 특정 시간에만 로직을 처리해주는 API에 대해 테스트 코드 작성이 필요했다.
그 외 시간에 요청이 오면 에러 응답하도록 되어있다.

  • 처리 가능 시간
    • 06시 ~ 12시 00분 30초
    • 18시 ~ 01시 00분 30초
// 참여 가능한 시간 체크
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime startLimit;
LocalDateTime endLimit;

if (time == 0) {
	startLimit = localDateTime.withHour(6).withMinute(0).withSecond(0).withNano(0);
	endLimit = localDateTime.withHour(12).withMinute(0).withSecond(30).withNano(0);
} else {
	startLimit = localDateTime.withHour(18).withMinute(0).withSecond(0).withNano(0);
	endLimit = localDateTime.plusDays(1).withHour(0).withMinute(0).withSecond(30).withNano(0);
}

if (!(localDateTime.compareTo(startLimit) >= 0 && localDateTime.compareTo(endLimit) <= 0)) {
	return ResponseCode.MISS_E001;
}

테스트할때만 유효성 체크하는 로직을 빼던가, 시간을 하드코딩하는 방법이 있겠지만
더 좋은 방법이 있다. 시간을 모킹하여 호출하면 굳이 비즈니스 로직을 건들지 않고 테스트 코드안에서 해결할 수 있다.

LocalDateTime 클래스 코드를 뒤져보면 다음과 같은 now 함수들을 확인할 수 있다.
보통 사용하는 now() 함수는 static이기 때문에 모킹하기 어렵지만
now(Clock clock) 함수는 Clock을 함수로 받기 때문에, 테스트시에만 Clock을 원하는 시간으로 조작이 가능할 것으로 보여진다.

public static LocalDateTime now() {
  return now(Clock.systemDefaultZone());
}

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);
}

2. 비즈니스 로직 변경

먼저 TimeConfig.java 라는 Configuration 클래스를 만든다.
모킹할 객체인 Clock 대해 Bean으로 등록해준다. 모킹하려면 모킹할 객체에 대해 Spring Context에 등록되어있어야 하기 때문이다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration
public class TimeConfig {

    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }
}

참여 시간을 체크하는 로직을 .now() 에서 .now(clock) 으로 바꿔준다.
만들었던 Clock 객체를 Autowired 로 가져와서 파라미터로 전달하면 된다.

@Service
@RequiredArgsConstructor
public class MissionService {

  private final Clock clock;

  @Transactional
  public ResponseCode tookEvent(String date, int time, SessionUser sessionUser) {
		...
    // 참여 가능한 시간 체크
    LocalDateTime localDateTime = LocalDateTime.now(clock);
		...
  }
}

비즈니스 로직 작업은 이게 끝이다.
이제는 테스트 코드를 작성해보도록 하자

3. 테스트 코드 작성

Clock 객체를 조작하기 위해 @SpyBean 으로 가져온다.
모킹하는 어노테이션으로는 @MockBean, @SpyBean 이 있는데
@SpyBean 은 객체 자체를 모킹하는 @MockBean 과 다르게, 필요한 특정 메서드만 모킹이 가능하다.

@SpyBean
private Clock clock;

LocalDateTime 에서 Instant를 가져올 때 clock.instant() 를 사용하므로, 이 메소드를 BDDMockito로 모킹하도록 하겠다.
BDDMockito는 BDD를 사용해서 테스트코드 작성시 시나리오에 맞게 테스트 코드가 읽히도록 개선된 Mockito Framwork이다.
Mockito를 상속하고 있고 기능도 동일하다. 다만, BDD 구조로 쉽게 읽힐 수 있도록 도와준다.

여기서 잠깐 BDD 구조에 대해 알아보자면
BDD란 Danial Terhorst-North와 Charis Matts가 TDD에서 착안한 방법론으로 행위 주도 개발(Behavior-Driven Development)을 말한다.
테스트 대상의 상태의 변화를 시나리오를 기반(Narrative)으로 테스트하는 패턴을 주로 사용한다. 이 때 권장하는 행동 패턴은 Given, When, Then 구조이다. 해당 패턴은 어떤 상태에서(Given) 어떤 행동을 했을 때(When) 어떤 결과가 되는 지(Then)를 테스트한다.

import static org.mockito.BDDMockito.given;
...
given(clock.instant())
  .willReturn(Instant.parse("2022-12-12T05:30:00Z"));

모킹 후, 원하는 대로 시간이 변경됐나 확인해보니 조금 이상하다.
지정한 시간보다 +9 만큼 시간이 더해져있다.

2022-12-12T14:30

이유는 ZoneOffset 이라는 시차 값이 UTC 기준 시간으로 설정되어있기 때문이다.
UTC는 세계 표준 시간으로써, 우리나라는 KST를 사용하는데 UTC보다 9시간이 빠르다.

그렇다면 아래와 같이 ZoneOffset을 KST로 맞춰준다.
날짜에 대한 값도 하드코딩이 아닌 오늘 날짜 기준으로 시간만 바꿔서 Instant로 변환하도록 수정한다.

LocalDate localDate = LocalDate.now();
ZoneOffset zoneOffset = ZoneOffset.of("+09:00");
given(clock.instant())
	.willReturn(localDate.atTime(5, 30).toInstant(zoneOffset));

다시 모킹 후 테스트를 실행해보면 시간이 정상 주입된 것을 확인할 수 있다.
이제는 자유롭게 api 요청 가능/불가능 시간 테스트를 손쉽게 진행할 수 있게 됐다.

2022-12-12T05:30

Leave a comment