| name | typescript-vitest |
| description | Use when writing tests, setting up test infrastructure, creating mocks, or configuring Vitest in TypeScript projects. Covers test organization, mocking patterns, assertions, and test utilities. |
Vitest Testing
Use Vitest as the test runner for all TypeScript projects.
Setup
Dependencies
npm install -D vitest @vitest/coverage-v8
Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
singleFork: true,
},
},
maxConcurrency: 2,
testTimeout: 20_000,
slowTestThreshold: 10_000,
bail: 1,
coverage: {
provider: 'v8',
},
expandSnapshotDiff: true,
},
})
With Path Aliases
If the project uses TypeScript path aliases (e.g., ~/), add resolve:
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
export default defineConfig({
resolve: {
alias: {
'~': resolve(__dirname, 'src'),
},
},
test: {
},
})
Test File Organization
Naming Convention
- Unit tests:
*.test.ts — colocated next to the source file
- Integration/E2E tests:
*.e2e.test.ts or placed in a tests/ directory
Directory Structure
src/
feature/
feature.ts
feature.test.ts # unit test next to source
tests/
mocks/ # shared mock utilities
mock-server.ts
mock-data.ts
helpers/ # test helpers
setup.ts
integration/ # integration tests
api.e2e.test.ts
Writing Tests
Basic Structure
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
describe('FeatureName', () => {
beforeEach(() => {
})
afterEach(() => {
})
it('should do expected behavior', () => {
const result = doSomething()
expect(result).toBe(expected)
})
it('should handle edge case', () => {
expect(() => doSomething(badInput)).toThrow('Expected error message')
})
})
Test Naming
Use descriptive names that explain the behavior, not the implementation:
it('should return empty array when no items match filter')
it('should throw when connection string is invalid')
it('should retry failed requests up to 3 times')
it('test filter')
it('error case')
it('works')
Assertions
Prefer Specific Matchers
expect(value).toBe(exact)
expect(obj).toEqual(expected)
expect(obj).toMatchObject(partial)
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()
expect(value).toBeGreaterThan(3)
expect(value).toBeCloseTo(0.3, 5)
expect(str).toContain('substring')
expect(str).toMatch(/regex/)
expect(arr).toHaveLength(3)
expect(arr).toContain(item)
expect(arr).toEqual(expect.arrayContaining([a, b]))
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('specific message')
expect(() => fn()).toThrow(CustomError)
await expect(asyncFn()).resolves.toBe(value)
await expect(asyncFn()).rejects.toThrow('error')
Inline Snapshots
Use inline snapshots for complex output verification:
expect(formatOutput(data)).toMatchInlineSnapshot(`
"expected output here"
`)
Mocking
Module Mocks
import { vi } from 'vitest'
vi.mock('./database', () => ({
query: vi.fn().mockResolvedValue([]),
connect: vi.fn(),
}))
vi.mock('./config')
Function Spies
const spy = vi.fn()
spy.mockReturnValue(42)
spy.mockResolvedValue(data)
spy.mockImplementation((x) => x * 2)
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledWith('arg1', 'arg2')
expect(spy).toHaveBeenCalledTimes(3)
Mock HTTP Server (Integration Tests)
For testing HTTP clients or APIs, create a mock server utility:
import { createServer, Server, IncomingMessage, ServerResponse } from 'http'
type MockResponse =
| { statusCode: 200; body: unknown }
| { statusCode: 204 }
| { statusCode: 400 | 404 | 500 | 503; body?: unknown }
export function createMockServer(responses: MockResponse[]) {
let callIndex = 0
const calls: { method: string; url: string; body: string }[] = []
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
const body = await readBody(req)
calls.push({ method: req.method!, url: req.url!, body })
const mock = responses[callIndex++]
if (!mock) {
res.writeHead(500)
res.end('No more mock responses')
return
}
res.writeHead(mock.statusCode, { 'Content-Type': 'application/json' })
if ('body' in mock && mock.body !== undefined) {
res.end(JSON.stringify(mock.body))
} else {
res.end()
}
})
return {
start: () => new Promise<string>((resolve) => {
server.listen(0, () => {
const addr = server.address() as { port: number }
resolve(`http://localhost:${addr.port}`)
})
}),
close: () => new Promise<void>((resolve) => server.close(() => resolve())),
calls,
}
}
Timer Mocks
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should debounce calls', async () => {
trigger()
vi.advanceTimersByTime(500)
expect(handler).toHaveBeenCalledTimes(1)
})
Test Utilities
Shared Setup
Create reusable test fixtures:
export function createTestContext() {
const logger = pino({ level: 'silent' })
const db = createTestDatabase()
return {
logger,
db,
async cleanup() {
await db.close()
},
}
}
Database Test Patterns
describe('UserRepository', () => {
let ctx: TestContext
beforeEach(async () => {
ctx = await createTestContext()
})
afterEach(async () => {
await ctx.cleanup()
})
it('should create user', async () => {
const user = await ctx.db.users.create({ name: 'Test' })
expect(user.id).toBeDefined()
})
})
Environment Variables in Tests
import { vi } from 'vitest'
beforeEach(() => {
vi.stubEnv('DATABASE_URL', 'postgres://localhost:5432/test')
})
afterEach(() => {
vi.unstubAllEnvs()
})
Package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}