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"],
},
});
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면 에러를 던진다", () => { /* ... */ });
});
});
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);
});
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);
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(); // 원래 타이머 복원
});
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() → 기존 객체의 메서드에 추적 기능을 덧붙임 (원본 유지)
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 | 실행된 문장의 비율 |
| Branches | if/else, 삼항 연산자 등 분기 중 실행된 비율 |
| Functions | 호출된 함수의 비율 |
| Lines | 실행된 소스 라인의 비율 |
HTML 리포트(coverage/index.html)를 열면 파일별, 라인별로 실행 여부를 시각적으로 확인할 수 있습니다.
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은 스냅샷을 별도 파일 대신 테스트 코드 안에 인라인으로 저장합니다. 작은 결과값을 즉시 확인할 때 편리합니다.