개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Vitest — Vite 기반 테스트 프레임워크

2026년 6월 17일
테스트ViteVitest

Vitest는 Vite 기반 프로젝트를 위해 설계된 테스트 프레임워크입니다. Vite의 설정과 변환 파이프라인을 그대로 재사용하므로, 별도의 Babel 설정 없이 TypeScript, JSX, 경로 별칭(@/)을 테스트 환경에서도 그대로 사용할 수 있습니다. API는 Jest와 호환되어 마이그레이션 부담이 작습니다.


설치 및 기본 설정

npm install -D vitest
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",   // 브라우저 환경 시뮬레이션 (React 컴포넌트 테스트 시)
    globals: true,          // describe, test, expect를 import 없이 사용
    setupFiles: ["./src/test/setup.ts"],
  },
});

핵심 개념

1. describe

describe는 관련된 테스트 케이스를 하나의 그룹으로 묶는 블록입니다. 테스트 파일에 여러 함수나 컴포넌트를 테스트할 때 구조적으로 분리할 수 있습니다.

import { describe, test, expect } from "vitest";
import { add, subtract } from "./math";

describe("add 함수", () => {
  test("양수 두 개를 더한다", () => {
    expect(add(1, 2)).toBe(3);
  });

  test("음수를 더한다", () => {
    expect(add(-1, -2)).toBe(-3);
  });
});

describe("subtract 함수", () => {
  test("큰 수에서 작은 수를 뺀다", () => {
    expect(subtract(5, 3)).toBe(2);
  });
});

describe는 중첩할 수 있습니다. 외부 describe가 모듈, 내부 describe가 메서드를 표현하는 패턴이 흔합니다.

describe("UserService", () => {
  describe("createUser", () => {
    test("이메일이 없으면 에러를 던진다", () => { /* ... */ });
    test("유효한 데이터로 유저를 생성한다", () => { /* ... */ });
  });

  describe("deleteUser", () => {
    test("존재하지 않는 ID면 에러를 던진다", () => { /* ... */ });
  });
});

2. test / it

test(또는 동의어인 it)는 개별 테스트 케이스를 정의하는 함수입니다. 첫 번째 인자는 테스트 설명, 두 번째 인자는 실행할 함수입니다.

// test와 it은 완전히 동일합니다
test("1 + 1은 2이다", () => {
  expect(1 + 1).toBe(2);
});

it("1 + 1은 2이다", () => {
  expect(1 + 1).toBe(2);
});

test.only로 특정 테스트만 실행하거나, test.skip으로 건너뛸 수 있습니다.

test.only("이 테스트만 실행", () => { /* ... */ });
test.skip("이 테스트는 건너뜀", () => { /* ... */ });

// 조건부 스킵
test.skipIf(process.env.CI === "true")("CI에서는 실행 안 함", () => { /* ... */ });

test.each로 여러 입력값을 반복 테스트할 수 있습니다.

test.each([
  [1, 2, 3],
  [0, 0, 0],
  [-1, 1, 0],
])("add(%i, %i) === %i", (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

3. expect

expect는 값을 검증하는 단언(assertion) 함수입니다. expect(실제값).매처(기대값) 형태로 사용하며, 다양한 매처를 체이닝할 수 있습니다.

// 원시값 동등 비교 (Object.is 사용)
expect(1 + 1).toBe(2);
expect("hello").toBe("hello");

// 객체·배열 깊은 비교 (재귀적으로 모든 프로퍼티 비교)
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
expect([1, 2, 3]).toEqual([1, 2, 3]);

// 포함 여부
expect([1, 2, 3]).toContain(2);
expect("hello world").toContain("world");

// 진위 확인
expect(true).toBeTruthy();
expect(null).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();

// 숫자 비교
expect(10).toBeGreaterThan(5);
expect(3.14).toBeCloseTo(3.141, 2); // 소수점 2자리까지 비교

// 에러 발생 확인
expect(() => { throw new Error("fail"); }).toThrow("fail");
expect(() => { throw new Error("fail"); }).toThrowError(Error);

// 부정: .not 체이닝
expect(1 + 1).not.toBe(3);
expect([1, 2]).not.toContain(5);

4. Mock

Mock은 외부 의존성(API 호출, 파일 I/O, 타이머 등)을 가짜 구현으로 대체하는 방법입니다. 테스트가 외부 환경에 의존하지 않도록 격리해 줍니다.

vi.fn으로 가짜 함수 생성:

import { vi, test, expect } from "vitest";

const mockFn = vi.fn();

// 반환값 설정
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: "hello" }); // Promise 반환

// 호출 후 검증
mockFn(1, 2);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(1, 2);

vi.mock으로 모듈 전체를 Mock:

import { vi, test, expect } from "vitest";
import { fetchUser } from "./api";

// 모듈 전체를 Mock으로 대체
vi.mock("./api", () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));

test("유저 데이터를 불러온다", async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe("Alice");
  expect(fetchUser).toHaveBeenCalledWith(1);
});

타이머 Mock:

test("1초 후 콜백이 실행된다", () => {
  vi.useFakeTimers();
  const callback = vi.fn();

  setTimeout(callback, 1000);
  expect(callback).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalledTimes(1);

  vi.useRealTimers(); // 원래 타이머 복원
});

5. Spy

Spy는 실제 구현을 유지하면서 함수의 호출 여부, 호출 횟수, 전달된 인자를 추적하는 방법입니다. Mock은 구현을 완전히 대체하지만, Spy는 원래 동작은 그대로 두고 감시만 합니다.

import { vi, test, expect } from "vitest";

const calculator = {
  add: (a: number, b: number) => a + b,
};

test("add가 올바른 인자로 호출된다", () => {
  // calculator.add의 실제 구현은 유지하면서 호출을 추적
  const spy = vi.spyOn(calculator, "add");

  const result = calculator.add(2, 3);

  // 실제 연산 결과를 반환
  expect(result).toBe(5);

  // 호출 추적 정보를 확인
  expect(spy).toHaveBeenCalledTimes(1);
  expect(spy).toHaveBeenCalledWith(2, 3);

  spy.mockRestore(); // 원래 상태로 복원
});

Spy는 구현 변경 없이 "이 함수가 올바른 시점에 올바른 인자로 호출됐는가"를 검증할 때 사용합니다.

Mock vs Spy

  vi.fn()         → 구현 없는 가짜 함수를 새로 만듦
  vi.mock()       → 모듈의 export를 통째로 가짜로 대체
  vi.spyOn()      → 기존 객체의 메서드에 추적 기능을 덧붙임 (원본 유지)

6. Coverage

Coverage는 테스트 코드가 실제 소스 코드를 얼마나 실행했는지 측정하는 지표입니다. 테스트가 통과하더라도 실제로 실행되지 않은 코드가 있을 수 있으므로, Coverage를 함께 확인해야 합니다.

npm install -D @vitest/coverage-v8

# 커버리지 측정과 함께 테스트 실행
npx vitest run --coverage
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      reporter: ["text", "html", "lcov"],
      thresholds: {
        lines: 80,    // 라인 커버리지 80% 미만이면 실패
        branches: 70,
        functions: 80,
        statements: 80,
      },
    },
  },
});

Coverage의 주요 측정 지표입니다.

지표의미
Statements실행된 문장의 비율
Branchesif/else, 삼항 연산자 등 분기 중 실행된 비율
Functions호출된 함수의 비율
Lines실행된 소스 라인의 비율

HTML 리포트(coverage/index.html)를 열면 파일별, 라인별로 실행 여부를 시각적으로 확인할 수 있습니다.


7. Snapshot

Snapshot 테스트는 컴포넌트나 함수의 출력을 파일로 저장해두고, 이후 실행 시 결과가 달라지면 실패시키는 방식입니다. UI 컴포넌트의 예상치 못한 변경을 감지하는 데 유용합니다.

import { test, expect } from "vitest";
import { render } from "@testing-library/react";
import Button from "./Button";

test("Button 스냅샷", () => {
  const { container } = render(<Button label="클릭" />);
  expect(container).toMatchSnapshot();
});

처음 실행하면 __snapshots__/Button.test.tsx.snap 파일이 생성됩니다.

// Button.test.tsx.snap (자동 생성)
exports[`Button 스냅샷 1`] = `
<div>
  <button
    class="btn"
  >
    클릭
  </button>
</div>
`;

이후 실행에서 출력이 달라지면 테스트가 실패합니다. 의도적인 변경이라면 스냅샷을 업데이트합니다.

# 스냅샷 업데이트
npx vitest run --update-snapshots

객체나 문자열에도 사용할 수 있습니다.

test("API 응답 스냅샷", async () => {
  const result = await fetchSummary();
  expect(result).toMatchInlineSnapshot(`
    {
      "count": 3,
      "items": ["a", "b", "c"],
    }
  `);
});

toMatchInlineSnapshot은 스냅샷을 별도 파일 대신 테스트 코드 안에 인라인으로 저장합니다. 작은 결과값을 즉시 확인할 때 편리합니다.

← 이전 글Vite — 빠른 개발 서버와 번들링을 제공하는 빌드 도구
다음 글 →Zod — 런타임 데이터 검증과 TypeScript 타입 추론