C.W.K.
Stream
Lesson 03 of 04 · published

Fake Timer: 시간을 멈춰 세우기

~16 min · vitest-mocking, fake-timers, time

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"토스트 사라지길 5초 기다리는 테스트는 네가 skip 할 테스트야."

왜 가짜 시간?

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

External links

Exercise

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.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.