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

vi.fn 과 vi.spyOn — 기억하는 함수들

~17 min · vitest-mocking, vi.fn, vi.spyOn

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Mock 은 자기가 호출됐다는 걸 기억하는 함수야."

vi.fn() — 빌딩 블록

vi.fn() 은 새 mock 함수를 반환해. Default 로 모든 호출에 undefined 반환하지만, 모든 호출을 inspect 가능한 .mock 객체에 기록해 — 인자, 반환값, this 바인딩까지. 그 기록이 이걸 mock 으로 만들어.

세 가족의 메서드로 구성해:

  • 반환값: 매 호출 mockReturnValue(x), 다음 호출만 mockReturnValueOnce(x) (queue 식).
  • 비동기 반환: mockResolvedValue(x), mockRejectedValue(err) — Promise 모양 반환의 sugar.
  • 완전한 구현: mockImplementation(fn) — mock 이 입력 기반으로 뭘 계산해야 할 때.

일어난 일 inspect 하기

대상 코드 돌고 나면 기록에 단언해. 지배적인 두 패턴:

  • expect(mock).toHaveBeenCalled() / toHaveBeenCalledTimes(n) — 호출됐어? 몇 번?
  • expect(mock).toHaveBeenCalledWith(args) / toHaveBeenLastCalledWith(args) — 어떤 인자 봤어?

더 외과적인 inspection 은 mock.calls 에 직접 손 넣어 — 호출당 한 항목, 인자 배열의 배열이야.

vi.spyOn — 함수가 이미 있을 때

vi.fn() 은 무에서 함수를 만들어. vi.spyOn(obj, 'method') 은 객체에 이미 있는 메서드를 wrap. Wrap 된 버전은 default 로 여전히 원본 호출하지만, 모든 호출이 기록되고, 필요하면 구현 오버라이드도 가능해.

킬러 feature 는 복원. spy.mockRestore() 가 원본 메서드를 다시 끼워. afterEach(() => vi.restoreAllMocks()) 와 짝지으면 다른 테스트로 spy 안 새.

관찰하되 교체 안 하려면 `vi.spyOn`. 의존성으로 넘길 새 fake 가 필요하면 `vi.fn()`. 둘 다 작동하는 경우에도 정신은 호환되지 않아 — 선택이 독자에게 의도를 신호해.

세 reset 메서드 (관례 하나 골라)

Vitest 가 비슷해 보이지만 다른 세 cleanup 메서드 줘:

  • mockClear() — 호출 이력 (mock.calls, mock.results) 지움. 구현과 반환값 queue 는 유지.
  • mockReset()mockClear() + 구현을 default (undefined 반환) 로 reset.
  • mockRestore() — spy 에만; 원본 메서드 복원. mockReset() 함의.

가장 깔끔한 관례: 모든 테스트 파일 맨 위에 (또는 vitest.setup.ts 에 전역으로) afterEach(() => vi.restoreAllMocks()) 두고, 수동 cleanup 신경 끊어. config 옵션 restoreMocks: true 가 같은 걸 암묵적으로 해줘.

Mock 은 default 로 새. 복원 잊은 spy 는 테스트 프로세스 나머지 동안 붙어있어. 같은 파일의 다음 테스트가 진짜 메서드 말고 spy 된 메서드에 대해 돌고, 너는 완전히 올바른 함수가 왜 undefined 반환하는지 디버깅하는 데 오후 하나 써. cleanup 은 프로젝트 레벨에서 한 번 세팅.

Code

vi.fn() — 기록, 구성, inspect·typescript
import { describe, it, expect, vi } from 'vitest';

describe('vi.fn() basics', () => {
  it('records every call', () => {
    const handler = vi.fn();

    handler('hello');
    handler('world', { important: true });

    expect(handler).toHaveBeenCalledTimes(2);
    expect(handler).toHaveBeenCalledWith('hello');
    expect(handler).toHaveBeenLastCalledWith('world', { important: true });

    // Surgical inspection
    expect(handler.mock.calls[0]).toEqual(['hello']);
    expect(handler.mock.calls[1][1].important).toBe(true);
  });

  it('can return configured values', () => {
    const getUser = vi.fn()
      .mockReturnValueOnce({ id: 1 })
      .mockReturnValueOnce({ id: 2 })
      .mockReturnValue({ id: 'fallback' });

    expect(getUser()).toEqual({ id: 1 });
    expect(getUser()).toEqual({ id: 2 });
    expect(getUser()).toEqual({ id: 'fallback' });
    expect(getUser()).toEqual({ id: 'fallback' });
  });

  it('can model async behavior', async () => {
    const fetchUser = vi.fn().mockResolvedValue({ name: 'Pippa' });
    await expect(fetchUser(1)).resolves.toEqual({ name: 'Pippa' });
  });
});
vi.spyOn — wrap, 관찰, 복원·typescript
import { describe, it, expect, vi, afterEach } from 'vitest';

afterEach(() => {
  vi.restoreAllMocks(); // single line, never leak
});

describe('vi.spyOn — observe without losing the original', () => {
  it('lets you check that console.warn was called', () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    callCodeThatWarns();

    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
    // After the test, restoreAllMocks puts console.warn back to normal.
  });

  it('can replace the method while still recording calls', () => {
    const fetch = vi.spyOn(global, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ ok: true }), { status: 200 })
    );

    return fetchUserById(42).then((user) => {
      expect(user.ok).toBe(true);
      expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/users/42'));
    });
  });
});
프로젝트 전체 cleanup config — 한 번 세팅, 영원히 안 샘·typescript
// In vitest.config.ts — set restoreMocks once, forget cleanup forever
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom',
    setupFiles: ['./vitest.setup.ts'],
    restoreMocks: true,   // auto-restore spies between tests
    clearMocks: true,     // auto-clear call history between tests
    // unstubGlobals: true, // also unstub vi.stubGlobal calls
  },
});

External links

Exercise

notify(user, channel) 함수 짜 — channel 인자 기반으로 sendEmail(user.email, msg), sendSMS(user.phone, msg), sendPush(user.deviceId, msg) 중 하나 호출. vi.fn() 으로 각 sender 만들어 세 번 테스트 — 맞는 sender 가 맞는 인자로 호출됐고, 나머지는 호출 안 됐는지 (toHaveBeenCalledTimes(0)) 단언. 보너스: 별개 vi.fn 대신 세 sender 다 가진 객체에 vi.spyOn 써봐.
Hint
한 큰 테스트에서 if (channel === 'email') expect(sendEmail).toHaveBeenCalled() 식으로 쓰고 있으면, 세 테스트로 쪼개. 각 테스트가 한 채널 이름 짓고 계약 단언.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

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

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