개요
Vite 기반의 테스트 프레임워크 Vitest 소개 및 실무 사용 패턴 정리 Jest와 거의 동일한 API를 제공하고, Vite의 ESM/HMR을 활용해 빠른 재실행을 제공함 Vite 프로젝트에서는 최소 설정으로 바로 사용 가능
- 빠른 실행과 재실행, watch 효율 높음
- Jest API 호환성 높아 러닝 커브 낮음
- TypeScript 네이티브 지원
1. 기본 구조
테스트 파일 명명 규칙
*.spec.ts 또는 *.test.tsdescribe(name, fn)
관련 테스트를 그룹화하는 컨테이너
import { describe, it, expect } from 'vitest'
describe('계산기', () => {
it('두 수를 더한다', () => {
expect(1 + 2).toBe(3)
})
it('두 수를 뺀다', () => {
expect(5 - 3).toBe(2)
})
})- 첫 번째 인자: 그룹 이름
- 두 번째 인자: 테스트 콜백
- 중첩 describe 구성 가능
it(name, fn) / test(name, fn)
개별 테스트 케이스 정의, 두 API는 동일 동작
it('유효한 이메일이면 true를 반환', () => {
expect(isValidEmail('test@example.com')).toBe(true)
})
test('유효한 이메일이면 true를 반환', () => {
expect(isValidEmail('test@example.com')).toBe(true)
})중첩 describe 출력 예
describe('UserService', () => {
describe('create', () => {
it('유효한 데이터로 유저 생성', () => { /* ... */ })
it('중복 이메일이면 에러', () => { /* ... */ })
})
describe('delete', () => {
it('존재하는 유저 삭제', () => { /* ... */ })
})
})UserService
create
✓ 유효한 데이터로 유저 생성
✓ 중복 이메일이면 에러
delete
✓ 존재하는 유저 삭제2. expect와 매처
expect(value)
검증 시작점, 다양한 매처 체이닝 지원
expect(실제값).matcher(기대값)기본 비교 매처
toBe(expected)
엄격 동등 비교 ===, 원시값 비교에 사용
expect(1 + 2).toBe(3)
expect('hello').toBe('hello')
expect(true).toBe(true)
// 객체/배열은 참조 비교라 실패
expect({ a: 1 }).toBe({ a: 1 }) // 실패
toEqual(expected)
깊은 동등 비교, 객체/배열 내용 비교
expect({ name: 'John', age: 30 }).toEqual({ name: 'John', age: 30 })
expect([1, 2, 3]).toEqual([1, 2, 3])
expect({ user: { name: 'John' }, items: [1, 2] }).toEqual({ user: { name: 'John' }, items: [1, 2] })toStrictEqual(expected)
toEqual보다 엄격, undefined 속성·희소 배열·클래스 인스턴스까지 체크
expect({ a: 1, b: undefined }).toEqual({ a: 1 })
expect({ a: 1, b: undefined }).toStrictEqual({ a: 1 }) // 실패
class User { name = 'John' }
expect(new User()).toEqual({ name: 'John' })
expect(new User()).toStrictEqual({ name: 'John' }) // 실패
Truthiness 매처
toBeTruthy / toBeFalsy
JavaScript truthy/falsy 규칙 기반 검증
// Truthy: 0, '', null, undefined, NaN, false 외의 모든 값
expect(1).toBeTruthy()
expect('hello').toBeTruthy()
expect([]).toBeTruthy()
expect({}).toBeTruthy()
// Falsy: 0, '', null, undefined, NaN, false
expect(0).toBeFalsy()
expect('').toBeFalsy()
expect(null).toBeFalsy()toBeNull / toBeUndefined / toBeDefined
명확한 의도 표현에 유용
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('value').toBeDefined()숫자 비교 매처
toBeGreaterThan / toBeLessThan / toBeGreaterThanOrEqual / toBeLessThanOrEqual
expect(10).toBeGreaterThan(5)
expect(10).toBeGreaterThanOrEqual(10)
expect(3).toBeLessThan(5)
expect(3).toBeLessThanOrEqual(3)toBeCloseTo(n, precision?)
부동소수점 오차 허용 비교, precision 기본값 2
expect(0.1 + 0.2).toBeCloseTo(0.3)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)문자열 매처
toMatch(regexp | string)
정규식 또는 부분 문자열 매칭
expect('hello world').toMatch(/world/)
expect('hello world').toMatch('world')
expect('hello world').toMatch(/^hello/)
expect('Error: 404').toMatch(/Error: \d+/)toContain(item)
문자열에 부분 문자열 포함 여부
expect('hello world').toContain('world')배열/이터러블 매처
toContain(item)
배열에 특정 요소 포함 여부
expect([1, 2, 3]).toContain(2)
const obj = { id: 1 }
expect([obj]).toContain(obj)
expect([{ id: 1 }]).toContain({ id: 1 }) // 실패: 참조 다름
toContainEqual(item)
배열 내 객체를 깊게 비교
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 })toHaveLength(n)
배열·문자열의 길이 검증
expect([1, 2, 3]).toHaveLength(3)
expect('hello').toHaveLength(5)
expect({ length: 3 }).toHaveLength(3)예외 검증 매처
toThrow(error?)
함수가 에러를 던지는지 검증, 함수 참조를 전달해야 함
expect(() => { throw new Error('fail') }).toThrow()
expect(() => { throw new Error('Invalid email') }).toThrow('Invalid email')
expect(() => { throw new Error('User 123 not found') }).toThrow(/not found/)
class ValidationError extends Error {}
expect(() => { throw new ValidationError('invalid') }).toThrow(ValidationError)잘못된 사용 예
// 즉시 실행되어 실패
expect(throwingFn()).toThrow()
// 올바른 사용
expect(() => throwingFn()).toThrow()rejects.toThrow 비동기 에러
async function asyncFail() { throw new Error('async error') }
await expect(asyncFail()).rejects.toThrow('async error')
await expect(Promise.reject(new Error('fail'))).rejects.toThrow()부정 not
모든 매처 앞에서 반대 조건 검증
expect(5).not.toBe(3)
expect([1, 2]).not.toContain(5)
expect(null).not.toBeDefined()
expect(() => safeFn()).not.toThrow()3. Setup과 Teardown
beforeAll(fn)
모든 테스트 전 1회 실행, DB 연결·서버 시작 등 공용 리소스 준비
import { beforeAll } from 'vitest'
let db: Database
beforeAll(async () => {
db = await Database.connect()
})afterAll(fn)
모든 테스트 후 1회 실행, 리소스 정리
import { afterAll } from 'vitest'
afterAll(async () => {
await db.disconnect()
})beforeEach(fn)
각 테스트 전 매번 실행, 상태 격리
import { beforeEach, vi } from 'vitest'
beforeEach(() => {
vi.clearAllMocks()
})afterEach(fn)
각 테스트 후 매번 실행, 정리 작업
import { afterEach } from 'vitest'
afterEach(() => {
cleanup()
})실행 순서 예시
beforeAll ← 1번
├── beforeEach ← 매번
│ └── 테스트 1
│ └── afterEach ← 매번
├── beforeEach ← 매번
│ └── 테스트 2
│ └── afterEach ← 매번
afterAll ← 1번4. Mock 함수
vi.fn(implementation?)
가짜 함수 생성, 호출 추적 및 반환값 제어 가능
import { vi } from 'vitest'
const mockFn = vi.fn()
const mockAdd = vi.fn((a, b) => a + b)
mockAdd(1, 2) // 3
사용 목적
- 외부 의존성 격리
- 호출 인자·횟수 검증
- 상황에 따른 반환값 제어
Mock 함수 메서드
mockReturnValue(value)
항상 동일 값 반환
const mockFn = vi.fn()
mockFn.mockReturnValue(42)
mockFn() // 42
mockReturnValueOnce(value)
지정된 호출 횟수만 특정 값 반환 후 기본 동작
const mockFn = vi.fn()
mockFn
.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValue(999)mockResolvedValue(value)
Promise.resolve(value) 반환
const mockFetch = vi.fn()
mockFetch.mockResolvedValue({ data: 'success' })
await mockFetch()동일 표현
mockFn.mockImplementation(() => Promise.resolve(value))mockResolvedValueOnce(value)
한 번만 resolve
mockFn
.mockResolvedValueOnce({ page: 1 })
.mockResolvedValueOnce({ page: 2 })mockRejectedValue(error)
Promise.reject(error) 반환
const mockFetch = vi.fn()
mockFetch.mockRejectedValue(new Error('Network error'))mockRejectedValueOnce(error)
한 번만 reject 후 정상 동작 설정 가능
mockFn
.mockRejectedValueOnce(new Error('일시적 에러'))
.mockResolvedValue({ success: true })mockImplementation(fn)
구현 자체 교체
const mockFn = vi.fn()
mockFn.mockImplementation((x, y) => x * y)조건 분기 예
mockHttpClient.get.mockImplementation(async (url, params) => {
if (params.page === 1) return { list: [1, 2], totalPage: 2 }
if (params.page === 2) return { list: [3, 4], totalPage: 2 }
return { list: [], totalPage: 2 }
})mockImplementationOnce(fn)
일회성 구현 교체
mockFn
.mockImplementationOnce(() => 'first')
.mockImplementationOnce(() => 'second')
.mockImplementation(() => 'default')Mock 초기화 메서드
mockClear()
호출 기록만 초기화, 구현은 유지
const mockFn = vi.fn().mockReturnValue(42)
mockFn()
mockFn.mockClear()mockReset()
호출 기록과 구현 초기화, 반환값 undefined로 복귀
const mockFn = vi.fn().mockReturnValue(42)
mockFn.mockReset()mockRestore()
원본 구현 복구, vi.spyOn으로 만든 spy 대상
const obj = { method: () => 'original' }
const spy = vi.spyOn(obj, 'method').mockReturnValue('mocked')
spy.mockRestore()전역 Mock 초기화
- vi.clearAllMocks() 모든 mock 호출 기록 초기화
- vi.resetAllMocks() 호출 기록과 구현 초기화
- vi.restoreAllMocks() 모든 spy 원복
beforeEach(() => {
vi.clearAllMocks()
})Mock 검증 매처
const mockFn = vi.fn()
mockFn()
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(1)
mockFn('hello', 123)
expect(mockFn).toHaveBeenCalledWith('hello', 123)
mockFn(1)
mockFn(2)
mockFn(3)
expect(mockFn).toHaveBeenLastCalledWith(3)
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello', 123)5. 모듈 Mocking
vi.mock(path, factory?)
모듈 전체를 mock으로 교체, import 이전에 가로채기 수행
import { vi } from 'vitest'
import { fetchData } from './api' // mock된 상태로 import
vi.mock('./api', () => ({
fetchData: vi.fn(),
saveData: vi.fn(),
}))동작 원리
- vi.mock은 파일 단위로 호이스팅되어 실제 import보다 먼저 실행
- 대상 모듈 로딩을 가로채 mock 버전으로 대체
- 이후 해당 모듈을 import하면 mock된 export 제공
Node 내장 모듈 mock
vi.mock('node:fs/promises', () => ({
readFile: vi.fn(),
writeFile: vi.fn(),
}))
it('파일 읽기 테스트', async () => {
const fs = await import('node:fs/promises')
vi.mocked(fs.readFile).mockResolvedValue('file content')
})부분 mock
일부만 교체하고 나머지는 실제 구현 유지
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>()
return {
...actual,
dangerousFunction: vi.fn(),
}
})6. vi.spyOn(object, method)
기존 메서드를 감시, 실제 구현 유지하며 호출·반환 추적 또는 선택적 교체
const calculator = { add: (a: number, b: number) => a + b }
const spy = vi.spyOn(calculator, 'add')
calculator.add(1, 2)
expect(spy).toHaveBeenCalledWith(1, 2)
expect(spy).toHaveReturnedWith(3)vi.fn vs vi.spyOn 비교
- vi.fn: 새 mock 함수 생성, 원본 없음, 복구 개념 없음
- vi.spyOn: 기존 메서드 감시, 원본 유지, mockRestore로 복구 가능
구현 교체와 복구
const spy = vi.spyOn(calculator, 'add')
spy.mockImplementation((a, b) => 0)
spy.mockRestore()7. vi.mocked(fn, deep?)
TypeScript 타입 단언 헬퍼, 런타임 영향 없음
import { fetchData } from './api'
vi.mock('./api')
// fetchData.mockResolvedValue(...) 는 타입 에러
vi.mocked(fetchData).mockResolvedValue({ data: 'test' })deep 옵션으로 중첩 메서드까지 mock 타입 적용
const mockService = { nested: { method: vi.fn() } }
vi.mocked(mockService, true).nested.method.mockReturnValue(1)8. 비동기 테스트
async/await
it('비동기 데이터를 가져온다', async () => {
const data = await fetchData()
expect(data).toEqual({ id: 1 })
})resolves / rejects
Promise 결과를 직접 검증
await expect(asyncFn()).resolves.toBe(42)
await expect(asyncFn()).resolves.toEqual({ data: 'success' })
await expect(failingFn()).rejects.toThrow('error')
await expect(failingFn()).rejects.toBeInstanceOf(ValidationError)9. 테스트 제어
it.only / describe.only
특정 테스트만 실행
it.only('이것만 실행', () => { /* ... */ })
describe.only('이 그룹만', () => { /* ... */ })it.skip / describe.skip
특정 테스트 건너뜀
it.skip('이건 건너뜀', () => { /* ... */ })
describe.skip('이 그룹 건너뜀', () => { /* ... */ })it.todo
향후 작성할 테스트 명시
it.todo('나중에 구현할 테스트')10. Best Practices
테스트 독립성 유지
// Bad: 테스트 간 상태 공유
let counter = 0
it('테스트1', () => { counter++ })
it('테스트2', () => { expect(counter).toBe(1) })
// Good: 매 테스트 초기화
beforeEach(() => {
vi.clearAllMocks()
})명확한 테스트 이름
// Bad
it('test1', () => { /* ... */ })
// Good
it('유효하지 않은 이메일이면 ValidationError를 던짐', () => { /* ... */ })
it('페이지가 2개 이상이면 모든 페이지 순회', () => { /* ... */ })AAA 패턴 적용
it('유저 생성 시 이메일 저장', async () => {
// Arrange
const userData = { email: 'test@example.com', name: 'John' }
mockRepository.save.mockResolvedValue({ id: 1, ...userData })
// Act
const result = await userService.create(userData)
// Assert
expect(result.email).toBe('test@example.com')
expect(mockRepository.save).toHaveBeenCalledWith(userData)
})필요한 것만 mock
// Bad: 사용하지 않는 메서드까지 mock
const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn(), trace: vi.fn() }
// Good: 필요한 메서드만
const mockLogger = { warn: vi.fn() } as unknown as Logger구현이 아닌 동작을 테스트
// Bad: 내부 구현 의존
expect(service['_internalArray']).toContain(item)
// Good: 공개 인터페이스 기준 검증
expect(service.getAll()).toContain(item)11. CLI 명령어
# 전체 테스트 실행
npx vitest run
# watch 모드
npx vitest
# 특정 파일만 실행
npx vitest user.service.spec.ts
# 테스트 이름 패턴 매칭
npx vitest --grep "유저 생성"
# 커버리지 리포트
npx vitest run --coverage
# UI 모드
npx vitest --ui마무리
Vitest는 Vite 생태계와 맞물려 빠른 피드백 루프를 제공함 Jest 호환 API를 활용해 기존 경험을 그대로 가져오면서, 모듈 mocking과 spy, 비동기 매처를 조합해 안정적인 단위 테스트 체계를 구축 가능