| name | typescript-testing |
| description | TypeScript testing patterns with Vitest and MSW. Use when writing unit tests, mocking APIs, creating typed mocks for dependency injection, or using snapshot testing. |
| user-invocable | false |
TypeScript Testing Patterns
Comprehensive testing patterns using Vitest for unit testing, MSW for API mocking, and snapshot testing for complex object validation.
Quick Start
Installation
bun add -d vitest @vitest/ui
bun add -d msw
bun add -d @vitest/coverage-v8
Basic Test Structure
import { describe, it, expect, vi, beforeEach } from 'vitest'
describe('UserService', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('creates user with valid data', async () => {
const result = await userService.create({ name: 'Alice' })
expect(result).toMatchObject({ name: 'Alice' })
})
})
Typed Mock Objects
Create type-safe mocks for dependency injection using vi.mocked():
import { vi } from 'vitest'
import type { UserRepository } from './user-repository'
vi.mock('./user-repository')
const mockUserRepo = vi.mocked<UserRepository>({
findById: vi.fn(),
save: vi.fn(),
delete: vi.fn(),
})
mockUserRepo.findById.mockResolvedValue({
id: '123',
name: 'Alice',
email: 'alice@example.com',
})
expect(mockUserRepo.findById).toHaveBeenCalledWith('123')
Mock Return Values
mockRepo.findById.mockResolvedValue(user)
mockRepo.findById
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(user)
mockRepo.findById.mockImplementation(async (id) => {
if (id === '123') return user
return null
})
mockRepo.save.mockRejectedValue(new Error('Database error'))
Spy on Real Implementations
import { vi } from 'vitest'
import { emailService } from './email-service'
vi.spyOn(emailService, 'send')
await emailService.send({ to: 'alice@example.com', subject: 'Test' })
expect(emailService.send).toHaveBeenCalledOnce()
emailService.send.mockResolvedValueOnce({ messageId: 'test-123' })
See references/mocking.md for comprehensive mocking patterns including module mocks, class mocks, stateful handlers, and dependency injection.
API Mocking with MSW
Mock HTTP requests at the network level for integration tests:
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll } from 'vitest'
const handlers = [
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Alice',
email: 'alice@example.com',
})
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: '123', ...body }, { status: 201 })
}),
]
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
See references/msw.md for advanced MSW patterns including error simulation, delays, and per-test overrides.
Snapshot Testing
Validate complex objects and output without manual assertions:
import { it, expect } from 'vitest'
it('generates correct user profile', () => {
const profile = generateUserProfile(user)
expect(profile).toMatchSnapshot()
})
it('creates order with timestamp', () => {
const order = createOrder(items)
expect(order).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date),
})
})
it('formats error message', () => {
const error = formatError(new Error('Failed'))
expect(error).toMatchInlineSnapshot(`
{
"message": "Failed",
"code": "UNKNOWN_ERROR",
}
`)
})
See references/snapshot-testing.md for snapshot maintenance, custom serializers, and best practices.
Testing Async Code
it('loads user data', async () => {
const user = await userService.findById('123')
expect(user).toBeDefined()
})
it('calls callback on completion', () => {
return new Promise<void>((resolve) => {
processData(data, (result) => {
expect(result).toBe(expected)
resolve()
})
})
})
it('retries after delay', async () => {
vi.useFakeTimers()
const promise = retryOperation()
vi.advanceTimersByTime(1000)
const result = await promise
expect(result).toBe('success')
vi.useRealTimers()
})
Test Organization
describe('UserService', () => {
describe('create', () => {
it('succeeds with valid data', () => {})
it('throws on duplicate email', () => {})
})
describe('update', () => {
it('updates existing user', () => {})
it('throws on not found', () => {})
})
})
describe('authenticated requests', () => {
beforeEach(() => {
mockAuth.isAuthenticated.mockReturnValue(true)
})
it('allows user creation', () => {})
it('allows user deletion', () => {})
})
Guidelines
- Test behavior, not implementation - Focus on what the code does, not how it does it
- One assertion per test when possible - Makes failures clearer and tests more focused
- Use typed mocks -
vi.mocked<T>() provides type safety for mock setup and assertions
- Mock at boundaries - Mock external dependencies (APIs, databases), not internal functions
- Use MSW for API tests - Mock at the network level for realistic integration tests
- Snapshots for complex output - Use snapshots for large objects, explicit assertions for critical values
- Property matchers for dynamic values - Handle dates, IDs, timestamps with
expect.any()
- Clear test names - Describe the scenario and expected outcome: "creates user with valid data"
- Setup/teardown for state - Use
beforeEach/afterEach to ensure test isolation
- Async/await over callbacks - Prefer
async/await for cleaner async test code
- Fake timers for time-based code - Use
vi.useFakeTimers() to control time in tests
- Test error cases - Verify error handling with
expect().rejects.toThrow()
Related Skills
- build-tools - Vitest configuration, test scripts, coverage setup
- api-design - Testing REST API contracts and error responses
- fastify - Testing Fastify routes and plugins
- drizzle-orm - Testing database queries (use MSW or in-memory DB)
- dynamodb-toolbox - Testing DynamoDB entities and queries
Common Patterns
Testing with Dependency Injection
class UserService {
constructor(
private repo: UserRepository,
private email: EmailService,
) {}
async create(data: CreateUserInput) {
const user = await this.repo.save(data)
await this.email.sendWelcome(user.email)
return user
}
}
it('sends welcome email on create', async () => {
const mockRepo = vi.mocked<UserRepository>({
save: vi.fn().mockResolvedValue(savedUser),
})
const mockEmail = vi.mocked<EmailService>({
sendWelcome: vi.fn().mockResolvedValue(undefined),
})
const service = new UserService(mockRepo, mockEmail)
await service.create(userData)
expect(mockEmail.sendWelcome).toHaveBeenCalledWith('alice@example.com')
})
Testing Error Boundaries
it('handles repository errors gracefully', async () => {
mockRepo.save.mockRejectedValue(new Error('Database error'))
await expect(
service.create(userData)
).rejects.toThrow('Failed to create user')
expect(mockEmail.sendWelcome).not.toHaveBeenCalled()
})
Testing Race Conditions
it('handles concurrent requests correctly', async () => {
const promises = [
service.processOrder(order1),
service.processOrder(order2),
service.processOrder(order3),
]
const results = await Promise.all(promises)
expect(results).toHaveLength(3)
expect(new Set(results.map(r => r.id)).size).toBe(3)
})
Debugging Tests
vitest run path/to/test.spec.ts
vitest run -t "creates user"
vitest watch
vitest --ui
vitest run --coverage