Vitest 와 테스트의 결
Vitest 는 Vite 생태계에서 자라난 테스트 프레임워크입니다. Jest 와 결을 비슷하게 두고 ESM · TypeScript · 워치 모드를 더 가볍게 다룹니다.
목차
Vitest 는 Vite 생태계에서 자라난 테스트 프레임워크입니다. Jest 와 결을 비슷하게 두고 ESM · TypeScript · 워치 모드를 더 가볍게 다룹니다.
1. Vitest 에 대한 이야기
Vite 팀 (Anthony Fu 외) 이 2021 년에 시작했고 1.0 이 2023 년에 공개됐습니다. 이후 활발히 변화 중입니다. 사이트는 vitest.dev, 라이선스는 MIT.
특징적인 자리:
- Vite 의 빠른 ESM·HMR 인프라 위에 동작.
- TypeScript 가 추가 설정 없이 동작.
- Jest 와 거의 호환되는 API (
describe·it·test·expect). - 워치 모드의 빠른 피드백.
- 같은 프로세스에서 여러 모듈을 동시 테스트.
- 브라우저 모드·in-source testing 같은 추가 기능.
2. Jest 와의 비교
Jest 는 Christopher Pojer · Meta 가 2014 년에 공개했습니다. 한 시기 React 진영의 사실상 표준 자리였습니다. CommonJS 출발이라 ESM 에서 추가 설정이 필요한 자리가 있고, Vite 환경과의 결합에 마찰이 있는 편입니다. 그래서 Vite 기반 프론트엔드 진영이 Vitest 로 옮겨가는 흐름이 있습니다.
대부분의 단언·매처 (toBe · toEqual · toHaveBeenCalled 등) 는 두 프레임워크에서 그대로 동작합니다.
3. 동시성과 격리
Vitest 는 워커 (스레드 또는 프로세스) 를 띄우고 테스트 파일을 분배합니다. 파일 단위 격리가 기본입니다.
[main] vitest CLI
├── worker-1 (a.test.ts)
├── worker-2 (b.test.ts)
└── worker-3 (c.test.ts)
테스트 안의 describe · test 는 같은 워커에서 순차 실행되지만 옵션에 따라 병렬도 가능합니다. 격리가 약해지면 테스트 간 간섭이 발생할 수 있습니다.
4. 워치 모드와 인소스 테스팅
vitest 또는 vitest --watch 로 시작하면 변경된 파일 + 그 파일에 의존하는 테스트만 다시 돕니다. 큰 코드베이스에서도 즉시 피드백이 가능한 자리입니다.
소스 파일 안에 if (import.meta.vitest) 블록으로 작은 테스트를 두는 옵션 (in-source testing) 도 있습니다. 작은 유틸 함수의 사용 예를 곁에 두는 자리에 어울립니다. 파일이 커지면 별도 테스트 파일로 옮기는 모양이 일반적입니다.
5. 모킹·커버리지
vi.fn() · vi.spyOn() · vi.mock() 의 모양이 Jest 와 비슷합니다. ESM 모킹은 CommonJS 보다 까다로운 자리가 있습니다 (자동 호이스팅 등). 도구별 가이드를 따릅니다. expect.poll · expect.assertions(n) 같은 비동기 보조가 있습니다.
커버리지는 v8 또는 istanbul 두 가지 백엔드를 옵션으로 가집니다. v8 은 빠르고 istanbul 은 보고가 풍부한 자리입니다. 임계값 (thresholds) 을 설정해 회귀를 잡습니다.
6. 다른 후보들
| 도구 | 결 |
|---|---|
| Jest | Meta · 2014. CommonJS 우선. |
| Vitest | Vite 팀 · 2021~. ESM · TypeScript 친화. |
| Mocha | OpenJS · 2011. 매처는 보통 chai 와 결합. |
| AVA | 2015. 병렬 우선. |
Node 내장 node:test |
2022 부터 표준 라이브러리. 가벼움. |
| Bun · Deno 내장 | 자체 테스트 러너 보유. |
E2E·통합 도구는 Playwright (Microsoft, 2020) · Cypress (2017) · Testing Library (Kent C. Dodds 등, 2018) · Storybook + Test runner. Vitest 는 단위·통합 자리, Playwright 는 브라우저 E2E 자리가 자연스럽습니다.
7. 빠른 시작
import { describe, it, expect } from "vitest";
import { sum } from "./sum";
describe("sum", () => {
it("두 수를 더한다", () => {
expect(sum(1, 2)).toBe(3);
});
});
설정 파일 (vitest.config.ts) 은 Vite 설정과 통합됩니다. 별도 빌드·트랜스파일 설정이 거의 없는 자리입니다.
React · Vue 컴포넌트는 @testing-library/{react,vue} 와의 결합이 잘 알려져 있습니다.
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
it("버튼을 누르면 1 증가한다", async () => {
render(<Counter />);
await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("1")).toBeInTheDocument();
});
8. 단위 vs 통합 vs E2E
- 단위 (unit) — 외부 의존이 없는 작은 함수·로직.
- 통합 (integration) — 모듈 합쳐 동작 검증. DB 가 필요하면 Testcontainers · in-memory.
- E2E — 브라우저·실제 환경. 별도 도구.
같은 폴더에 섞으면 피드백 시간이 늘어납니다. 디렉터리 또는 파일명 컨벤션으로 분리합니다.
9. 테스트의 비용
테스트는 자유롭게 더 추가할수록 좋은 자리가 아닙니다. 다음 비용이 함께 자랍니다.
- 유지보수 — 코드 변경 시 테스트도 함께 고칩니다. 잘못 짠 테스트는 변경을 가로막습니다.
- 실행 시간 — 통합·E2E 가 늘면 CI 가 길어집니다.
- 거짓 신호 — 의도와 어긋난 테스트는 회귀 알람을 흐립니다.
- 결합도 — 구현 세부에 묶인 테스트는 리팩터를 막습니다.
10. 테스트는 사양이라는 시각
테스트 코드는 "이 함수는 이렇게 동작해야 한다" 의 실행 가능한 사양입니다. 사양으로 읽히도록 이름·구조를 짭니다. 매처보다 테스트 이름이 먼저입니다.
it("빈 배열은 0 을 반환한다", () => {});
it("음수가 포함되면 무시한다", () => {});
기능 단위 (behavior) 테스트는 외부에서 본 행동을 검증합니다. 리팩터에 강합니다. 단위 단위 (structure) 테스트는 내부 함수·메서드 단위입니다. 정밀하지만 결합도가 높습니다.
기능 단위 비중을 키우고 단위 단위는 위험·복잡 함수에 한정하는 모양이 일반적입니다.
11. 어디에 더 둘까
가치가 높은 자리:
- 결제·과금·인증·데이터 손실 같은 비가역 자리.
- 도메인 핵심 로직 (가격 계산·권한·상태 머신).
- 회귀가 자주 났던 자리.
가벼운 표현 컴포넌트의 픽셀 일치 검증보다 도메인 로직의 경계 케이스가 더 가치가 있다는 평이 다수입니다.
12. 자주 걸리는 자리
눈에 띄는 커버리지 100% — 행 커버리지가 의미 있는 회귀 보호와 동치는 아닙니다.
Jest → Vitest 이주 — API 가 비슷하지만 ESM 모킹·타이머·환경 차이로 일부 테스트가 깨집니다.
테스트 환경 차이 — jsdom · happy-dom · 브라우저 모드의 차이. DOM API 일부가 누락될 수 있습니다.
시간·랜덤 의존 — Date.now · Math.random 직접 사용 테스트는 깨집니다. vi.useFakeTimers() · 의존성 주입.
공유 상태 — 모듈 최상위에서 만든 객체가 테스트끼리 공유돼 간섭합니다. 격리·beforeEach 리셋.
실행 순서 의존 — 어떤 테스트가 다른 테스트를 셋업하는 의존. 무작위 순서로 돌려도 통과해야 합니다.
모킹 과잉 — 모든 의존을 모킹하면 결국 모킹의 정합성을 테스트하게 됩니다. 진짜 의존 (가능한 경우) 또는 통합 테스트.
하고픈 말
테스트는 코드 작성보다 코드 변경에 더 큰 비용이 듭니다. 변경에 강한 테스트는 인터페이스 단위로, 약한 테스트는 구현 세부 단위로 갈립니다. 사양 단위로 쓰는 습관이 가장 큰 차이를 만듭니다.
실전 사례를 기록하는 방법
추상 원칙은 특정 저장소의 고유명사보다 재현 가능한 실패 조건으로 기록해야 합니다. 다음 사례는 어떤 웹·API·워커 조합에도 적용할 수 있습니다.
모듈 mock의 export 표면
실 모듈에 새 함수가 추가됐는데 테스트 mock이 그 함수를 내보내지 않으면, 타입 검증을 통과해도 첫 호출에서 실패할 수 있습니다.
// ❌ 실제 모듈의 export를 일부만 복제
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));
// ✅ 호출하는 표면까지 명시
vi.mock('next/cache', () => ({
revalidatePath: vi.fn(),
revalidateTag: vi.fn(),
}));
vi.hoisted와 mock contract 테스트로 실 모듈의 공개 표면을 고정합니다.
동기 API를 비동기로 바꾸는 일괄 변경
인증 검증처럼 비동기 provider로 바꾸는 작업은 문자열 치환만으로 끝나지 않습니다. 안전한 순서는 다음과 같습니다.
- 작은 범위에서 함수명과 호출부를 바꿉니다.
tsc --noEmit으로 await 누락과 Promise 타입 불일치를 찾습니다.- 호출자까지 async 경계를 전파하고 단위·통합 테스트를 실행합니다.
- 변경 전후의 오류 코드와 latency를 비교합니다.
컴파일러는 일괄 변경의 첫 회귀 탐지기이며, 실제 인증 공급자와 read-only smoke가 최종 증거입니다.
통합 테스트 비용을 계층화하기
컨테이너를 많이 부팅하는 통합 테스트는 PR 피드백을 늦출 수 있습니다. PR에서는 컴파일·라우팅·인증 필터·5xx 차단을 빠르게 확인하고, 전체 컨테이너 실행은 nightly 또는 명시적 운영 환경에서 수행합니다.
private void assertRouted(int status) {
assertTrue(status >= 200 && status < 600, "routing abnormal status=" + status);
}
이 규칙은 payload 검증을 대체하지 않으므로, read-only endpoint의 응답 계약과 별도 계층에서 보완합니다.
테스트 파일도 production build의 입력
개발용 test runner가 허용하는 미사용 import가 production tsc 단계에서는 오류가 될 수 있습니다. noUnusedLocals, lint, tsc --noEmit, production build를 같은 변경에서 실행하고, 실패한 명령·마지막 성공 단계·재실행 방법을 기록하세요.
Next
- observability-minimal
- vitest-pytest-infra
Vitest 공식 · Jest 공식 · Node test runner · Testing Library · Playwright · Test Pyramid (Martin Fowler) 을 참고합니다.
quality 카테고리의 다른 글
카테고리 전체 보기 →관련 글
테스트 층과 도구 선택 — Vitest·pytest·Cypress·Playwright
단위·통합·브라우저·운영 smoke 검증을 구분하고, Vitest·pytest·Cypress·Playwright를 겹치지 않게 배치하는 기준을 정리합니다.
HTTP API Mocking — WireMock · MockServer · Prism · MSW
에뮬레이터가 없는 외부 SaaS가 있습니다. OAuth 로그인 · 결제 · 동적 DNS · Slack Webhook · AI provider, 그리고 Firebase Cloud Messaging처럼 공식 emulator가 없는 서비스도 있습니다. 이런 외부 의존을 끊고 싶을 때 가장 단순한 길은 HTTP 응답을 흉내내는…
Firebase Local Emulator Suite — Firebase 한 묶음을 노트북에
Firebase 는 2011 년 작은 스타트업으로 시작해 2014 년 Google 에 인수됐습니다. 매니지드 모델이 강한 서비스답게 한동안은 "로컬에서 띄우는 길이 없다" 가 맞는 답이었지만, 2020 년 3 월 Firebase Local Emulator Suite 가 정식 GA 되면서 그림이 바뀌었습니다.
LocalStack 과 MiniStack — 로컬에서 AWS 흉내내기
AWS 대부분은 인터넷 너머 마법의 데이터센터에서 돈을 받고 일합니다. 개발 머신에서 검증하려면 (1) 실제 AWS 에 PoC 계정 만들기, (2) 어딘가에 비용을 결제하기, (3) 인터넷이 죽으면 손이 멈추기 — 세 가지 비용을 매일 치러야 합니다. 로컬 에뮬레이터는 이 세 비용을 0 에 가깝게 만듭니다.