16.1 Vitest + TypeScript — Fast Unit Testing
What is Vitest?
Vitest is an ultra-fast test framework built on Vite. It natively supports TypeScript and provides a Jest-compatible API, so existing Jest tests work as-is.
npm install --save-dev vitest @vitest/coverage-v8
vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // Use describe, it, expect globally
environment: 'node', // Or 'jsdom' for browser environment
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
},
},
include: ['**/*.{test,spec}.{ts,tsx}'],
},
})
Basic Test Writing
// math.ts
export function add(a: number, b: number): number {
return a + b
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Cannot divide by zero.')
return a / b
}
export async function fetchUser(id: string): Promise<{ id: string; name: string }> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('User not found.')
return response.json()
}
// math.test.ts
import { describe, it, expect } from 'vitest'
import { add, divide } from './math'
describe('add', () => {
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3)
expect(add(-1, 1)).toBe(0)
expect(add(0.1, 0.2)).toBeCloseTo(0.3)
})
it('handles large numbers', () => {
expect(add(1_000_000, 2_000_000)).toBe(3_000_000)
})
})
describe('divide', () => {
it('performs division', () => {
expect(divide(10, 2)).toBe(5)
expect(divide(7, 2)).toBe(3.5)
})
it('throws error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero.')
expect(() => divide(10, 0)).toThrowError(Error)
})
})
Matcher Usage
import { describe, it, expect } from 'vitest'
describe('Matcher examples', () => {
it('basic matchers', () => {
// Equality
expect(1 + 1).toBe(2) // Primitive comparison (===)
expect({ a: 1 }).toEqual({ a: 1 }) // Deep comparison
expect({ a: 1 }).toStrictEqual({ a: 1 }) // Type-strict comparison
// Truthy/Falsy
expect(true).toBeTruthy()
expect(null).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect(0).toBeDefined()
// Numbers
expect(10).toBeGreaterThan(5)
expect(10).toBeLessThanOrEqual(10)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)
// Strings
expect('Hello World').toContain('World')
expect('typescript').toMatch(/script/)
// Arrays/Objects
expect([1, 2, 3]).toContain(2)
expect([1, 2, 3]).toHaveLength(3)
expect({ name: 'Alice', age: 30 }).toMatchObject({ name: 'Alice' })
})
it('error matchers', () => {
const throwError = () => { throw new TypeError('Type error') }
expect(throwError).toThrow()
expect(throwError).toThrow(TypeError)
expect(throwError).toThrow('Type error')
})
})
Mock Functions (vi.fn)
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Service interface
interface EmailService {
sendWelcome(to: string): Promise<void>
sendReset(to: string, token: string): Promise<void>
}
// Test target — dependency injection
class UserRegistrationService {
constructor(private emailService: EmailService) {}
async register(email: string) {
// Business logic...
await this.emailService.sendWelcome(email)
return { email, createdAt: new Date() }
}
}
describe('UserRegistrationService', () => {
let mockEmailService: EmailService
let service: UserRegistrationService
beforeEach(() => {
mockEmailService = {
sendWelcome: vi.fn().mockResolvedValue(undefined),
sendReset: vi.fn().mockResolvedValue(undefined),
}
service = new UserRegistrationService(mockEmailService)
})
it('sends welcome email on registration', async () => {
await service.register('alice@example.com')
expect(mockEmailService.sendWelcome).toHaveBeenCalledOnce()
expect(mockEmailService.sendWelcome).toHaveBeenCalledWith('alice@example.com')
})
it('returns registered user info', async () => {
const result = await service.register('alice@example.com')
expect(result).toMatchObject({ email: 'alice@example.com' })
expect(result.createdAt).toBeInstanceOf(Date)
})
})
Module Mocking (vi.mock)
// user.service.ts
import { prisma } from './lib/prisma'
export async function getUserById(id: string) {
return prisma.user.findUnique({ where: { id } })
}
// user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getUserById } from './user.service'
// Module mocking — declare before import
vi.mock('./lib/prisma', () => ({
prisma: {
user: {
findUnique: vi.fn(),
},
},
}))
import { prisma } from './lib/prisma'
describe('getUserById', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns user', async () => {
const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' }
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any)
const result = await getUserById('1')
expect(result).toEqual(mockUser)
expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { id: '1' } })
})
it('returns null when not found', async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue(null)
const result = await getUserById('999')
expect(result).toBeNull()
})
})
Timer Mocking
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function retryWithDelay<T>(fn: () => Promise<T>, retries: number): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await fn()
} catch {
if (i < retries - 1) await delay(1000 * (i + 1))
}
}
throw new Error('Max retries exceeded')
}
describe('retryWithDelay', () => {
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it('returns result on success', async () => {
const fn = vi.fn().mockResolvedValue('success')
const result = await retryWithDelay(fn, 3)
expect(result).toBe('success')
expect(fn).toHaveBeenCalledOnce()
})
it('retries after failure', async () => {
const fn = vi.fn()
.mockRejectedValueOnce(new Error('First failure'))
.mockResolvedValue('success')
const promise = retryWithDelay(fn, 3)
await vi.runAllTimersAsync()
const result = await promise
expect(result).toBe('success')
expect(fn).toHaveBeenCalledTimes(2)
})
})
Pro Tips
Snapshot Testing
import { describe, it, expect } from 'vitest'
function formatUser(user: { name: string; email: string; role: string }) {
return `[${user.role.toUpperCase()}] ${user.name} <${user.email}>`
}
describe('formatUser', () => {
it('matches snapshot', () => {
const result = formatUser({ name: 'Alice', email: 'alice@example.com', role: 'admin' })
expect(result).toMatchInlineSnapshot(`"[ADMIN] Alice <alice@example.com>"`)
})
})
package.json Script Setup
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}