Vitest 단위 테스트 실전: 설정부터 TDD 개발 흐름까지

ESM 프로젝트에서 Jest를 설정하려면 얼마나 많은 시간을 써야 할까요? ts-jest, babel-jest, jest.config.js에 각종 모듈 해석 문제까지 처리해야 합니다. 저도 Jest가 .vue 파일의 import 문을 제대로 인식하게 만드는 데만 오후 내내 시간을 쓴 적이 있습니다.
반면 Vitest에는 설정 한 줄이면 충분합니다.
과장이 아닙니다. Vite 프로젝트에서 처음 vitest를 실행했을 때 테스트 케이스가 몇 초 만에 끝나는 모습을 보니, 3년 동안 버벅이던 오래된 컴퓨터를 바꾼 듯한 상쾌함이 느껴졌습니다.
Vitest는 얼마나 빠를까요? 공식 데이터와 커뮤니티 실측에 따르면 콜드 스타트는 약 200ms이고 Jest는 2~4초가 걸립니다. 테스트 케이스 500개는 약 8초 만에 끝나는 반면 Jest는 약 45초가 필요합니다. 더 매력적인 점은 Vite와 설정을 공유하고 TypeScript를 네이티브로 지원하며 API가 Jest와 거의 같다는 것입니다. 마이그레이션 비용은 30분 정도일 수도 있습니다.
이 글에서는 처음부터 설정하는 방법과 완전한 TDD 흐름, Mocking 기법, Coverage 설정을 다룹니다. 새 프로젝트에서 Vitest를 사용하려는 경우든 기존 프로젝트를 Jest에서 옮기려는 경우든 필요한 내용을 찾을 수 있습니다.
Vitest란 무엇이며 왜 이렇게 빠를까요?
간단히 말해 Vitest는 Vite 네이티브 테스트 프레임워크입니다.
이미 Vite로 프로젝트를 빌드하고 있다면 Vitest는 사실상 바로 사용할 수 있습니다. 별칭, 환경 변수, CSS 처리 등 Vite 설정을 그대로 재사용해 모두 자동으로 상속합니다. Jest에서 필요했던 transform, moduleFileExtensions, moduleNameMapper 같은 추가 설정도 필요 없습니다.
핵심 장점은 세 가지입니다.
빠른 속도. 콜드 스타트는 약 200밀리초이며 Jest는 보통 2~4초가 걸립니다. 대규모 프로젝트에서는 차이가 더 뚜렷합니다. 테스트 케이스 500개를 Vitest는 약 8초에 실행하지만 Jest는 약 45초가 걸립니다(DEV Community의 2026년 실측 데이터). 결코 작은 차이가 아닙니다.
Jest 호환성. API가 거의 같습니다. describe, it, expect, vi.fn()의 사용 방식이 Jest와 다르지 않습니다. Jest에서 마이그레이션할 때는 대부분 import 경로만 바꾸면 됩니다.
지능형 감시. Vitest에는 “HMR for tests”라는 기능이 있습니다. 코드를 수정하면 전체 테스트를 다시 실행하는 대신 관련된 테스트만 다시 실행합니다. 개발할 때 즉시 피드백을 받는 경험이 아주 좋습니다.
무엇인지 알았으니 이제 설치 방법을 살펴보겠습니다.
설치 및 설정
먼저 시스템 요구 사항을 확인해야 합니다. Vite는 6.0.0 이상, Node는 20.0.0 이상이어야 합니다. 비교적 최근에 만든 프로젝트라면 대부분 충족할 것입니다.
설치
명령 한 줄이면 됩니다.
npm install -D vitest
이게 전부입니다. @types/jest, ts-jest, jest-environment-jsdom 같은 패키지를 잔뜩 설치할 필요가 없습니다. Vitest는 TypeScript를 네이티브로 지원합니다.
설정 파일
두 가지 방법이 있습니다. vite.config.ts에 test 필드를 추가하거나 별도의 vitest.config.ts 파일을 만들면 됩니다.
프로젝트가 단순하다면 vite.config.ts를 바로 사용하세요.
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // 전역 변수로 설정해 매번 import { describe, it, expect }를 작성하지 않음
environment: 'node', // 또는 'jsdom'(브라우저 환경 테스트)
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
})
globals: true는 매우 유용합니다. 이 옵션을 켜면 describe, it, expect가 모두 전역 변수가 되어 각 테스트 파일에서 일일이 import하지 않아도 됩니다. Jest와 같은 방식입니다.
테스트에 DOM API가 필요하다면(예: 컴포넌트 렌더링 테스트) environment를 'jsdom'으로 바꾸고 jsdom을 설치하세요.
npm install -D jsdom
실행 스크립트
package.json에 두 줄을 추가합니다.
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
npm test는 감시 모드로 진입해 코드를 수정할 때 자동으로 다시 실행합니다. npm run test:run은 한 번 실행한 뒤 종료하므로 CI 환경에서 사용합니다.
설정은 이것뿐입니다. Jest에서 필요했던 수많은 preset, transform, moduleFileExtensions 설정과 비교하면 훨씬 간단합니다.
단위 테스트 작성하기
테스트 파일 이름에는 보통 .test.ts 또는 .spec.ts를 사용합니다. tests/ 디렉터리에 두거나 원본 파일과 같은 위치에 놓을 수 있으며 팀 관례에 맞추면 됩니다.
기본 구조
가장 간단한 테스트는 다음과 같습니다.
import { describe, it, expect } from 'vitest'
import { add, divide } from './math'
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('should throw on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero')
})
})
describe는 테스트를 그룹화하고, it은 개별 테스트 케이스를 정의하며, expect는 단언을 수행합니다.
globals: true를 켰다면 import 문은 생략할 수 있습니다.
자주 쓰는 단언
가장 자주 쓰는 단언은 다음과 같습니다.
// 기본 동등 비교
expect(value).toBe(5) // 엄격한 동등 비교(===)
expect(obj).toEqual({ a: 1 }) // 깊은 동등 비교
// 참/거짓 판정
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
// 예외
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')
// 숫자 비교
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)
// 배열/문자열 포함 여부
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)
테스트 필터링
개발 중 특정 테스트만 실행하고 싶다면 .only를 사용합니다.
it.only('이 테스트만 실행', () => { ... })
테스트 하나를 임시로 건너뛰려면 .skip을 사용합니다.
it.skip('임시로 실행하지 않음', () => { ... })
두 메서드는 describe에도 적용됩니다. describe.only(...), describe.skip(...)처럼 사용하면 됩니다.
이제 실행해 보세요. 감시 모드에서는 코드를 수정한 직후 테스트 결과가 통과 또는 실패로 바뀌는 것을 확인할 수 있습니다. 이런 즉각적인 피드백은 시작할 때마다 몇 초씩 기다려야 하는 Jest보다 훨씬 나은 경험을 제공합니다.
TDD 개발 흐름 실전
TDD(테스트 주도 개발)의 핵심 개념은 간단합니다. 테스트를 먼저 작성하고 코드는 나중에 작성합니다.
직관에 어긋나는 것처럼 들릴 수 있지만, 실제로 해 보면 코드를 작성하기 전에 “이 함수가 무엇을 해야 하는지” 먼저 명확히 생각하게 됩니다. 코드를 다 작성한 뒤 테스트를 보충하는 방식은 테스트를 커버리지 숫자만 채우는 형식적인 작업으로 만들기 쉽습니다.
이제 실전 사례로 전체 흐름을 살펴보겠습니다. 이메일 검증 함수 validateEmail을 구현해 보겠습니다.
Step 1: 아직 코드를 구현하지 않은 상태에서 테스트 작성
먼저 테스트 파일을 새로 만듭니다.
// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'
describe('validateEmail', () => {
it('should return true for valid email', () => {
expect(validateEmail('[email protected]')).toBe(true)
})
it('should return false for invalid email', () => {
expect(validateEmail('invalid')).toBe(false)
})
})
이 시점에는 validateEmail 함수가 없으므로 테스트를 실행하면 당연히 오류가 납니다. 하지만 괜찮습니다. 이것이 바로 TDD의 첫 단계인 테스트 실패 만들기입니다.
Step 2: 최소한의 코드 구현
이제 함수를 만들고 테스트를 통과할 수 있는 가장 간단한 구현을 작성합니다.
// src/validateEmail.ts
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
npm test로 테스트를 실행합니다.
두 테스트가 모두 통과했나요? 그렇다면 Step 2가 끝났습니다.
Step 3: 경계 조건 테스트 추가
기본 테스트는 통과했지만 이메일 검증에는 많은 경계 조건이 있습니다. 몇 가지를 추가해 보겠습니다.
// tests/validateEmail.test.ts (추가)
it('should return false for empty string', () => {
expect(validateEmail('')).toBe(false)
})
it('should return false for email without domain', () => {
expect(validateEmail('user@')).toBe(false)
})
it('should return false for email with spaces', () => {
expect(validateEmail('test @example.com')).toBe(false)
})
테스트를 실행합니다. 모두 통과하면 정규식이 잘 작성된 것입니다. 실패하는 테스트가 있다면 정규식을 수정합니다.
Step 4: 리팩터링
테스트가 모두 통과했으니 이제 안심하고 코드를 리팩터링할 수 있습니다. 예를 들어 정규식을 더 엄격한 버전으로 바꾸거나 주석을 추가할 수 있습니다.
// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export function validateEmail(email: string): boolean {
if (!email || email.trim() === '') {
return false
}
return EMAIL_REGEX.test(email)
}
수정한 뒤 테스트를 다시 실행해도 모두 통과합니다. 이것이 TDD의 장점입니다. 테스트가 안전망이 되어 리팩터링 과정에서 실수하면 바로 알 수 있습니다.
TDD가 유용한 이유
솔직히 저도 처음에는 “테스트를 먼저 작성한다”는 방식이 익숙하지 않았습니다. 하지만 몇 번 사용해 보니 다음 장점을 알게 됐습니다.
- 먼저 생각하고 구현합니다. 테스트를 작성하는 과정 자체가 함수의 동작을 설계하는 일이라서 입력과 출력을 명확히 정리하게 됩니다.
- 빠르게 반복할 수 있습니다. Vitest 감시 모드에서는 코드를 바꾸고 몇 초 안에 피드백을 받을 수 있어 기다릴 필요가 없습니다.
- 안전하게 리팩터링할 수 있습니다. 테스트가 충분히 갖춰져 있으면 코드를 바꿀 때 불안하지 않습니다.
유틸리티 함수나 포맷 함수처럼 간단한 함수부터 시도해 보세요. 익숙해진 뒤 복잡한 로직으로 확장하면 됩니다.
고급 Mocking 기법
단위 테스트에서는 API 요청, 데이터베이스 조회, 서드파티 라이브러리 같은 외부 의존성을 가짜로 대체해야 할 때가 많습니다. 이럴 때 Mock을 사용합니다.
Vitest는 Jest와 비슷한 Mock API를 제공하며 핵심은 vi 객체입니다.
vi.fn(): 단일 함수 Mock 처리
가장 간단한 사용법은 가짜 함수를 만드는 것입니다.
import { vi, describe, it, expect } from 'vitest'
describe('vi.fn() demo', () => {
it('tracks calls', () => {
const mockFn = vi.fn()
mockFn('hello')
mockFn('world')
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
})
})
반환 값을 미리 지정할 수도 있습니다.
const mockFn = vi.fn().mockReturnValue('mocked result')
// 또는 비동기 반환 값
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })
vi.mock(): 모듈 전체 Mock 처리
테스트하려는 함수가 외부 API에 의존하나요? 해당 모듈을 바로 Mock 처리하면 됩니다.
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'
// api 모듈 Mock 처리
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))
describe('UserService', () => {
beforeEach(() => {
vi.clearAllMocks() // 각 테스트 전에 이전 호출 기록 삭제
})
it('should fetch user', async () => {
const service = new UserService()
const user = await service.getUser(1)
expect(user.name).toBe('Alice')
expect(fetchUser).toHaveBeenCalledWith(1)
})
})
vi.mock()은 모듈을 가져오기 전에 실행되므로 파일 상단에 둡니다.
vi.spyOn(): 실제 함수 감시
함수를 완전히 교체하지 않고 호출만 감시하고 싶을 때도 있습니다.
import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'
describe('spyOn demo', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('tracks add calls', () => {
const addSpy = vi.spyOn(calculator, 'add')
const result = calculator.add(2, 3)
expect(result).toBe(5) // 원래 함수가 정상 실행됨
expect(addSpy).toHaveBeenCalledWith(2, 3) // 호출 기록도 함께 남김
})
})
spyOn은 mock보다 온건합니다. 함수는 평소처럼 동작하고 감시 기능만 추가됩니다.
전역 객체 Mock 처리
테스트에서 실제 HTTP 요청을 보내고 싶지 않다면 전역 fetch를 Mock 처리합니다.
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'mocked' })
}))
또는 Vitest의 vi.stubGlobal을 사용해 window, localStorage 같은 브라우저 전역 객체를 Mock 처리할 수 있습니다.
Mock은 테스트에서 가장 까다로운 부분입니다. 간단한 vi.fn()부터 시작해 익숙해진 뒤 vi.mock()을 사용하는 것이 좋습니다. 각 테스트가 끝난 뒤에는 반드시 Mock 상태를 정리하세요(clearAllMocks 또는 restoreAllMocks). 그렇지 않으면 서로 다른 테스트가 상태를 공유해 오염될 수 있습니다.
Coverage와 모범 사례
테스트 커버리지는 코드 품질을 평가하는 참고 지표입니다. Vitest는 v8(더 빠르고 네이티브 지원)과 istanbul(호환성이 더 좋음)이라는 두 가지 Coverage provider를 지원합니다. 보통은 v8이면 충분합니다.
Coverage 설정
vite.config.ts에 다음 내용을 추가합니다.
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'], // 출력 형식
thresholds: {
lines: 80, // 행 커버리지 임계값
functions: 80, // 함수 커버리지 임계값
branches: 70, // 분기 커버리지 임계값
},
exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
},
}
다음 명령을 실행합니다.
vitest run --coverage
터미널에 커버리지 보고서가 표시되고 coverage/ 디렉터리도 생성됩니다. 그 안의 HTML 보고서를 열면 어느 코드가 커버되지 않았는지 확인할 수 있습니다.
임계값 설정의 의미
thresholds는 단순한 장식이 아닙니다. 커버리지가 설정값에 도달하지 못하면 Vitest는 오류와 함께 종료합니다. CI 환경에서 일정한 테스트 품질을 강제하고 대충 작성한 코드가 제출되는 것을 막는 데 유용합니다.
물론 임계값을 지나치게 높게 설정할 필요는 없습니다. 80%가 합리적인 출발점이며, 오히려 100%는 팀에 불필요한 부담을 줄 수 있습니다.
CI/CD 통합
GitHub Actions나 다른 CI에 다음 단계를 추가합니다.
# .github/workflows/test.yml
- name: Run tests with coverage
run: npm run test:run -- --coverage
실행이 끝나면 lcov 보고서를 Codecov나 Coveralls에 업로드해 커버리지 변화를 시각적으로 추적할 수 있습니다.
몇 가지 실용적인 조언
- 100%에 집착하지 마세요. 커버리지 숫자가 좋다고 코드 품질까지 높은 것은 아닙니다. 핵심 로직을 테스트하고 일부 경계 상황은 넘어가도 됩니다.
- 핵심 경로부터 테스트하세요. 주요 흐름의 테스트 우선순위가 가장 높고 예외 분기는 그다음입니다.
- 불필요한 테스트를 정기적으로 정리하세요. 테스트도 유지보수가 필요합니다. 오래되거나 중복된 테스트는 삭제하세요.
- Watch 모드를 습관화하세요. 개발할 때
vitest감시 실행을 유지하면 문제를 즉시 발견할 수 있습니다.
정리
Vitest의 핵심 장점은 결국 빠르고, 간편하고, 사용하기 좋다는 것입니다.
빠릅니다. 콜드 스타트는 200ms이며 테스트 케이스 500개도 8초 만에 실행해 Jest보다 거의 10배 빠릅니다. 간편합니다. Vite와 설정을 공유하고 설치 직후 실행할 수 있어 복잡한 transform이나 moduleNameMapper를 설정할 필요가 없습니다. 사용하기 좋습니다. API가 Jest와 거의 같아 마이그레이션 비용이 매우 낮습니다.
Vite를 사용한다면 Vitest를 바로 선택하세요. Jest의 ESM 설정 문제로 다시 씨름할 이유가 없습니다.
프로젝트에서 여전히 Jest를 사용한다면 30분 정도 투자해 마이그레이션을 시도해 보세요. 먼저 Vitest를 설치하고 테스트 파일의 import를 수정하면 대부분 바로 실행할 수 있습니다.
TDD도 너무 복잡하게 생각하지 마세요. 간단한 유틸리티 함수부터 시작해 테스트를 먼저 작성하고 코드를 나중에 구현하면 됩니다. 익숙해지면 이처럼 먼저 생각하고 구현하는 방식이 오히려 더 효율적이라는 것을 알게 됩니다.
테스트는 부담이 아니라 안전장치입니다. 시간을 조금 들여 Vitest를 제대로 설정하면 이후에는 더 안심하고 코드를 작성할 수 있습니다.
Vitest 단위 테스트 설정과 TDD 흐름
Vitest를 처음부터 설정하고 TDD 개발 흐름을 실습하는 전체 단계
⏱️ Estimated time: 30 min
- 1
Step 1: Vitest 설치
설치 명령을 실행합니다:
```bash
npm install -D vitest
```
시스템 요구 사항: Vite >= 6.0.0, Node >= 20.0.0 - 2
Step 2: vite.config.ts 설정
설정 파일에 test 필드를 추가합니다:
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
},
})
```
globals: true를 사용하면 describe, it, expect를 매번 import하지 않아도 됩니다. - 3
Step 3: 실행 스크립트 추가
package.json에 다음 내용을 추가합니다:
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
```
npm test는 감시 모드이며, npm run test:run은 한 번만 실행합니다(CI용). - 4
Step 4: 첫 테스트 작성
테스트 파일 tests/math.test.ts를 만듭니다:
```typescript
import { describe, it, expect } from 'vitest'
describe('Math', () => {
it('should add numbers', () => {
expect(1 + 1).toBe(2)
})
})
```
npm test를 실행해 설정이 제대로 되었는지 확인합니다. - 5
Step 5: TDD 흐름 실습
테스트 주도 개발 흐름을 따릅니다:
• Step 1: 먼저 테스트를 작성해 함수의 기대 동작을 정의합니다.
• Step 2: 테스트를 통과할 최소한의 코드를 구현합니다.
• Step 3: 경계 조건 테스트를 추가합니다.
• Step 4: 코드를 리팩터링합니다.
Vitest 감시 모드를 사용하면 몇 초 안에 피드백을 받을 수 있습니다. - 6
Step 6: Coverage 설정
커버리지 설정을 추가합니다:
```typescript
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
thresholds: {
lines: 80,
functions: 80,
},
}
```
vitest run --coverage를 실행해 보고서를 생성합니다.
FAQ
Vitest와 Jest는 무엇이 다른가요?
Jest에서 Vitest로 어떻게 마이그레이션하나요?
• Jest 관련 패키지를 제거하고 vitest를 설치합니다.
• jest.config.js 설정을 vite.config.ts로 옮깁니다.
• 테스트 파일의 import { describe, it, expect } from 'jest'를 from 'vitest'로 변경합니다.
• jest.fn(), jest.mock()을 vi.fn(), vi.mock()으로 변경합니다.
대부분의 경우 30분 안에 마이그레이션을 마칠 수 있습니다.
Vitest는 어떤 테스트 환경을 지원하나요?
Vitest에서 API 요청을 어떻게 Mock 처리하나요?
• vi.fn(): 단일 함수를 Mock 처리하고 반환 값을 미리 지정합니다.
• vi.mock(): 모듈 전체를 Mock 처리해 모든 export를 교체합니다.
• vi.spyOn(): 실제 구현을 교체하지 않고 함수 호출을 감시합니다.
각 테스트가 끝난 뒤 vi.clearAllMocks() 또는 vi.restoreAllMocks()으로 상태를 정리해야 합니다.
Vitest Coverage는 어떻게 설정하나요?
TDD 개발 흐름의 핵심은 무엇인가요?
• Red: 먼저 실패하는 테스트를 작성합니다.
• Green: 테스트를 통과할 최소한의 코드를 작성합니다.
• Refactor: 코드 구조를 개선합니다.
Vitest 감시 모드를 함께 사용하면 코드를 바꾼 뒤 몇 초 안에 피드백을 받아 빠르게 반복할 수 있습니다. 테스트를 먼저 작성하면 함수 설계를 명확히 정리하는 데 도움이 됩니다.
4분 읽기 · 게시일: 2026년 4월 14일 · 수정일: 2026년 9월 4일
Vitest 테스트 가이드
이 시리즈의 첫 글을 읽고 있습니다. 다음 글로 이어가거나 시리즈 허브에서 전체 경로를 확인하세요.
이전
이 시리즈의 시작입니다.
다음
현재 이 시리즈의 최신 글입니다.



댓글
GitHub로 로그인하여 댓글을 남기세요