본문으로 바로가기

Vitest 와 테스트의 결

Vitest 는 Vite 생태계에서 자라난 테스트 프레임워크입니다. Jest 와 결을 비슷하게 두고 ESM · TypeScript · 워치 모드를 더 가볍게 다룹니다.

6회 조회약 6분 읽기
X에 공유 새 창에서 열림
목차

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로 바꾸는 작업은 문자열 치환만으로 끝나지 않습니다. 안전한 순서는 다음과 같습니다.

  1. 작은 범위에서 함수명과 호출부를 바꿉니다.
  2. tsc --noEmit으로 await 누락과 Promise 타입 불일치를 찾습니다.
  3. 호출자까지 async 경계를 전파하고 단위·통합 테스트를 실행합니다.
  4. 변경 전후의 오류 코드와 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 카테고리의 다른 글

카테고리 전체 보기 →

관련 글

LocalStack 과 MiniStack — 로컬에서 AWS 흉내내기

약 5분

AWS 대부분은 인터넷 너머 마법의 데이터센터에서 돈을 받고 일합니다. 개발 머신에서 검증하려면 (1) 실제 AWS 에 PoC 계정 만들기, (2) 어딘가에 비용을 결제하기, (3) 인터넷이 죽으면 손이 멈추기 — 세 가지 비용을 매일 치러야 합니다. 로컬 에뮬레이터는 이 세 비용을 0 에 가깝게 만듭니다.

이 글이 도움이 되었나요?