setTimeout, setInterval, requestAnimationFrame, 또는 Date.now() / new Date() 읽는 코드는 시간 결합돼 있어. auto-dismiss 토스트가 5,000ms 후 사라지면, 실제 테스트는 5,000ms 기다려. suite 에 timer 인식 테스트 50개 곱하면 CI 가 1분에서 1시간으로 바뀌어.
Fake timer 는 진짜 timer API 를 제어 가능한 버전으로 교체. setTimeout 은 여전히 콜백 queue 에 넣지만 알아서 발동 안 해 — 수동으로 vi.advanceTimersByTime(5000) 으로 시간 진행, 그 윈도우 안에 스케줄된 콜백 다 동기적으로 발동. 5초 걸리던 테스트가 5밀리초에 끝나.
Setup / Teardown 패턴
Fake timer 는 전역 — 한 번 켜면 모든 협력자의 모든 timer 가 끌 때까지 fake clock 써. 깔끔한 패턴은 테스트별 또는 suite 별:
beforeEach(() => vi.useFakeTimers()) — 각 테스트 전에 활성화.
afterEach(() => vi.useRealTimers()) — 각 테스트 후 복원.
Cleanup 잊으면 다음 테스트가 요청 안 한 frozen clock 을 받아 — "왜 이 완전히 무관한 테스트가 멈춰?" 디버깅으로 몇 시간 잡아먹어.
세 advance 메서드
vi.advanceTimersByTime(ms) — 시계 ms 만큼 앞으로, 그 윈도우에 스케줄된 모든 콜백 발동. 가장 흔해.
vi.runAllTimers() — 보류 중인 모든 timer 한 번에 실행, setInterval 도 (queue 빌 때까지). 모든 보류 작업 완료시키고 싶을 때 유용.
vi.runOnlyPendingTimers() — 현재 queue 된 timer 만 실행, 그 콜백이 스케줄한 새 거 무시. 재귀 setTimeout 루프 빠져나올 때 유용.
Fake timer 는 microtask 를 자동 진행 안 해. Timer 콜백이 promise 를 await 하면 microtask 도 flush 해야 해 — await vi.advanceTimersByTimeAsync(5000) (Async 변형) 가 둘 다 처리. 이거 틀리면 timer 는 발동되는데 promise 가 resolve 안 되는 걸 보게 돼.
Date 도 가짜로
Date.now() (또는 new Date()) 를 직접 읽는 시간 결합 코드도 fake clock 후보. vi.setSystemTime('2026-01-01') 가 wall clock 을 특정 순간에 고정 — 모든 읽기가 그 순간과 일관된 값 반환. vi.useFakeTimers 와 결합하면 완전히 결정적인 시간 환경 얻어.
Code
토스트 dismiss — 즉각 테스트, 5초 동작·typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// Code under test
export function autoDismissToast(onDismiss: () => void, ms = 5000) {
setTimeout(onDismiss, ms);
}
// Tests
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
describe('autoDismissToast', () => {
it('does not dismiss before the duration elapses', () => {
const onDismiss = vi.fn();
autoDismissToast(onDismiss, 5000);
vi.advanceTimersByTime(4999);
expect(onDismiss).not.toHaveBeenCalled();
});
it('dismisses exactly at the duration', () => {
const onDismiss = vi.fn();
autoDismissToast(onDismiss, 5000);
vi.advanceTimersByTime(5000);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});
Debounce — fake timer 의 완벽한 use case·typescript
// Debounce — only fire after 300ms of quiet
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
export function debounce<T extends (...a: any[]) => void>(fn: T, wait = 300) {
let id: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
if (id) clearTimeout(id);
id = setTimeout(() => fn(...args), wait);
};
}
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
describe('debounce', () => {
it('only fires once for a burst of calls within the wait window', () => {
const onFire = vi.fn();
const debounced = debounce(onFire, 300);
debounced('a');
vi.advanceTimersByTime(100);
debounced('b');
vi.advanceTimersByTime(100);
debounced('c');
vi.advanceTimersByTime(300);
expect(onFire).toHaveBeenCalledTimes(1);
expect(onFire).toHaveBeenCalledWith('c');
});
});
비동기 timer — `advanceTimersByTimeAsync` 가 promise 도 flush·typescript
// Async timers — flush both timers AND microtasks
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
export async function delayedFetch(url: string, ms: number) {
await new Promise((resolve) => setTimeout(resolve, ms));
return fetch(url);
}
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('completes the fetch after the delay', async () => {
vi.spyOn(global, 'fetch').mockResolvedValue(
new Response('ok', { status: 200 })
);
const promise = delayedFetch('/api/health', 1000);
await vi.advanceTimersByTimeAsync(1000); // async version flushes microtasks too
const response = await promise;
expect(response.status).toBe(200);
});
`setSystemTime` — wall clock 핀 고정·typescript
// setSystemTime — for code that reads Date.now() directly
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime('2026-01-01T00:00:00Z');
});
afterEach(() => vi.useRealTimers());
it('greets the user with the current year', () => {
function greet() {
return `Happy ${new Date().getFullYear()}, traveler.`;
}
expect(greet()).toBe('Happy 2026, traveler.');
// Advance the system clock one year.
vi.setSystemTime('2027-06-15T00:00:00Z');
expect(greet()).toBe('Happy 2027, traveler.');
});
retryWithBackoff(fn, attempts, baseMs) 짜 — fn 을 최대 attempts 번 재시도, 매 retry 마다 delay 2배 (baseMs, baseMs*2, baseMs*4, ...). 세 테스트 짜: (1) 첫 시도 성공 — 지연 불필요, (2) 세 번째 시도 성공 — vi.advanceTimersByTime 으로 지수 지연 검증, (3) 모든 retry 소진 — throw 검증. (2) 의 테스트는 시뮬레이션된 대기가 1+2+4=7초여도 밀리초에 완료돼야 해.
Hint
fn 이 async 면 vi.advanceTimersByTimeAsync 쓰고 promise await 잊지 마. Microtask flush 가 다음 retry 가 실제로 발동되게 해줘.
Progress
Progress is local-only — sign in to sync across devices.