이번 포스트에서는 setTimeout 이나 setInterval 사용 환경 즉 Timer에 대한 테스팅에 대해 살펴보자.
Timer Test
테스트 대상 함수 준비
먼저 테스트 할 함수를 준비해보자. 전체적인 테스트는 sync와 async 환경에서 테스트 한다.
let interval500 = 0, timeout2000 = 0 // 동작 회수를 카운트하기 위한 변수
let intervalId // interval 작업을 중지하기 위한 변수
const mywork = () => {
setTimeout(() => {
console.log('timeout 2000', timeout2000++)
}, 2000)
intervalId = setInterval(() => {
console.log('interval 500', interval500++)
}, 500)
}
편의상 2000ms 후 동작하는 timeout 작업은 timeout2000, 500ms 마다 동작하는 interval 작업은 interval500이라고 부르자.
위의 mywork에 대한 async 버전도 아래처럼 준비해보자.
const myworkAsync = () => {
setTimeout(() => {
Promise.resolve().then(() => console.log('async timeout 2000', timeout2000++))
}, 2000)
intervalId = setInterval(() => {
if (interval500 > 10) {
clearInterval(intervalId)
} else {
Promise.resolve().then(() => console.log('async interval 500', interval500++))
}
}, 500)
}
sync 버전과는 달리 Promise를 활용하며 interval 작업에서 interval500이 10보다 커지면 작업을 중지시켰는데 이는 무한 interval 테스트 시 error가 발생하는데 이상하게 assertion이 되지 않아서 궁여지책으로 infinite 상황을 제외하기 위한 조치이다.
테스트 준비
timer에 대해 임의로 제어해야 하기 때문에 모든 테스트 작업에서 fake timer를 사용하도록 설정해야 한다.
// fake time에서 처리
beforeEach(() => {
vi.useFakeTimers()
interval500 = 0
timeout2000 = 0
})
afterEach(() => clearInterval(intervalId))
테스팅
다음은 vitest에서 제공하는 다양한 테스트 utility의 사용 예이다.
주어진 시간까지 타이머 동작
advanceTimersByTime와 advanceTimersByTimeAsync는 각각 동기와 비동기 Timer에 대해서 주어진 시간이 지난 후의 상태를 점검할 수 있게 도와준다.
정확히 3초가 지난 후 timeout2000은 1번 동작하고 interval500은 6번 동작했을 것이다.
test('주어진 시간까지 타이머 동작(동기)', () => {
mywork()
vi.advanceTimersByTime(3000)
expect(interval500).toBe(6)
expect(timeout2000).toBe(1)
})
test('주어진 시간까지 타이머 동작(비동기)', async () => {
myworkAsync()
await vi.advanceTimersByTimeAsync(3000)
expect(interval500).toBe(6)
expect(timeout2000).toBe(1)
})
시간 순으로 다음 타이머 동작
advanceTimersToNextTimer와 advanceTimersToNextTimerAsync는 각각 동기와 비동기 Timer에 대해서 Timer Queue의 내용을 하나씩 순서대로 실행해준다.
예를 들어 4번의 nextTimer를 호출한다면 시간 순으로 interval500 > interval500 > interval500 > interval500 & timeout2000이 동작한다.
test('시간 순으로 다음 타이머 동작 실행(동기)', () => {
mywork()
vi.advanceTimersToNextTimer().advanceTimersToNextTimer()
.advanceTimersToNextTimer().advanceTimersToNextTimer()
expect(interval500).toBe(4)
expect(timeout2000).toBe(1)
})
test('시간 순으로 다음 타이머 동작 실행(비동기)', async () => {
myworkAsync()
for (let i = 0; i < 4; i++) {
// await로 호출해야 하는데 chaining 안되나?
await vi.advanceTimersToNextTimerAsync()
}
expect(interval500).toBe(4)
expect(timeout2000).toBe(1)
})
advanceTimersToNextTimer는 chaining 방식으로 호출되는데 advanceTimersToNextTimerAsync는 안되는것 같다. await 때문인가?
모든 대기중인 타이머가 끝날 때까지 진행
runAllTimers와 runAllTimersAsync는 각각 동기와 비동기 상황에서 모든 타이머가 끝날때 까지 진행한다. 단 무한 타이머의 경우 10,000번까지 시도하고 예외를 발생시킨다.
test('모든 대기중인 타이머가 끝날때 까지 진행. 무한대의 경우 10000 번까지만하고 실패(동기)', () => {
mywork()
expect(() => vi.runAllTimers()).toThrowError(
'Aborting after running 10000 timers, assuming an infinite loop!'
)
})
test('모든 대기중인 타이머가 끝날때 까지 진행. 무한대의 경우 10000 번까지만(비동기)', async () => {
myworkAsync()
await vi.runAllTimersAsync()
expect(interval500).toBe(11)
expect(timeout2000).toBe(1)
})
그런데 runAllTimers의 경우 예외가 잘 검출이 되었는데 runAllTimersAsync의 경우는 Function이 반환되었다. 그래서 myworkAsync에서는 10보다 크면 작업을 중지시켜 버렸다. 뭔가 또 놓친것이 있나 ㅜㅜ 예외 검증이 잘 되시는 분 알려주세요. ~~
모든 대기중인 타이머가 1번은 끝날때 까지만 진행
timeout2000이 끝나려면 최소 2000ms가 필요하다 이 시간이면 interval500은 4번 진행될 것이다. runOnlyPendingTimers나runOnlyPendingTimersAsync는 각각 동기/비동기에서 모든 타이머들이 최소 한번은 진행한 후에 종료한다.
test('모든 대기중인 타이머가 1번 끝날때 까지만 진행 - 가장 큰 시간(동기)', () => {
mywork()
vi.runOnlyPendingTimers()
expect(interval500).toBe(4)
expect(timeout2000).toBe(1)
})
test('모든 대기중인 타이머가 1번 끝날때 까지만 진행 - 가장 큰 시간(비동기)', async () => {
myworkAsync()
await vi.runOnlyPendingTimersAsync()
expect(interval500).toBe(4)
expect(timeout2000).toBe(1)
})