Spring

Spring Scheduler 테스트 하기

voider 2022. 11. 28. 23:48

Spring Scheduler Test

Spring Scheduler 사용법은 매우 간단하다. SpringBoot는 스타터에 스케줄러를 내장하고 있기 때문에 별도의 의존성을 추가하지 않고도 사용할 수 있다. 초간단 Spring Scheduler 적용에서 스케줄러를 동작시키는 방법을 정리했는데, cron expression 말고도 fixedDelay 속성을 이용하면 지정한 주기로 스케줄러를 실행시킬 수 있다.

@Scheduled(cron = "0 0,30 3 * * *") // 매일 3시, 0시 30분
fun publishReward() {
    log.info("START === PUBLISH::REWARD::EVENT::DAILY::SCHEDULER ${UUID.randomUUID()}")

    rewardService.publish(
        request = RewardPublishRequest(
            publishCycle = PublishCycle.DAILY
        )
    )

    log.info("END === PUBLISH::REWARD::EVENT::DAILY::SCHEDULER ${UUID.randomUUID()}")
}

스케줄러는 스프링 빈으로 관리되기 때문에 이것이 정말로 실행되는지 테스트하기 위해서는 스프링 컨테이너를 올리는 통합테스트가 필요하다. 만약 30초마다 동작하는 스케줄러라면 스레드를 30초 동안 멈춰서 스케줄러가 실행될 때까지 기다렸다가, 제대로 실행되었는지를 검증해볼 수도 있을 것이다. Awaitility라는 라이브러리를 사용하여 스레드를 멈추는 것보다 좀더 선언적으로 테스트를 진행할 수도 있다. 둘 중 어느 방법을 선택하더라도 무방해보이지만, 좀더 선언적으로 테스트를 할 수 있게 돕는 라이브러리인 Awaitility가 더 많이 사용되는 듯하다. 아래와 같은 형태로 사용할 수 있다.

@Test
fun `이틀 치 리워드를 미리 발행하는 스케줄러 동작하면 내일, 모레 날짜로 각각 10개씩 총 20개 발행한다`() {
    val beforeRewards = rewardService.getRewards(event.id)

    assertThat(beforeRewards.size).isEqualTo(0)

    Awaitility.await()
        .atMost(3, TimeUnit.SECONDS)
        .untilAsserted {
            val afterRewards = rewardService.getRewards(event.id)
            assertThat(afterRewards.isNotEmpty()).isTrue
            assertThat(afterRewards.size).isEqualTo(20)
        }

}

그런데 30초마다 실행되는 스케줄러를 테스트하기 위해서 30초를 대기해야 할까? 그럼 위 예제처럼 매일 새벽 3시에 실행되는 스케줄러를 실행하기 위해서는 테스트를 진행할 수 없을 것이다. 이 문제를 해결하기 위해서 Spring Properties를 사용할 수 있다.

@Scheduled(cron = "\${schedules.cron.reward.publish}") // 매일 3시, 0시 30분
fun publishReward()

이렇게 cron expression에도 스프링 설정 파일을 읽어와서 적용할 수 있다.

application.yml

schedules:
  cron:
    reward:
      publish: 0 0,30 3 * * *

이렇게 프로퍼티 파일에 크론 식을 지정하면 런타임에 이 설정을 읽어서 스케줄러를 동작시킨다. 테스트 환경에서는 이 시간을 조작하여 더 빠르게 실행시킬 수 있다.

직접 테스트 환경에 맞는 프로퍼티 파일을 만들 수도 있고, 간단하게 @SpringBootTest 속성을 이용하여 크론 식을 지정할 수 있다.

@SpringBootTest(
    properties = [
        "schedules.cron.reward.publish=0/2 * * * * ?",
    ]
)
class RewardPublishSchedulerTest {

이렇게 구성하면 런타임에서는 새벽 3시에 동작하지만, 테스트 환경에서는 2초마다 동작하기 때문에 30초나 혹은 그보다 더 긴 시간을 기다릴 필요 없이 최대한 빠르게 테스트하는 것이 가능하다.

아래 테스트는 3초를 대기한 다음 스케줄러가 예상대로 동작해서 원하는 결과를 만들어냈는지 검증한다.

@Test
fun `이틀 치 리워드를 미리 발행하는 스케줄러 동작하면 내일, 모레 날짜로 각각 10개씩 총 20개 발행한다`() {
    val beforeRewards = rewardService.getRewards(event.id)

    assertThat(beforeRewards.size).isEqualTo(0)

    Awaitility.await()
        .atMost(3, TimeUnit.SECONDS)
        .untilAsserted {
            val afterRewards = rewardService.getRewards(event.id)
            assertThat(afterRewards.isNotEmpty()).isTrue
            assertThat(afterRewards.size).isEqualTo(20)
        }

}

atMost(3, TimeUnit.SECONDS) : 3초를 기다린다.
untilAsserted: 스케줄러가 동작한 다음 상태를 검증한다.

Awaitility는 이 외에도 여러 기능이 있는 것 같으니 필요한 부분은 찾아서 적용하면 될 것 같다.