Mocking Pattern
Mock dependencies effectively in tests using Vitest mock functions, module mocking, timers, and MSW for HTTP requests.
Overview#
Mocking isolates code under test by replacing dependencies with controlled implementations. This enables testing units in isolation and simulating various scenarios.
When to use:
- Isolating code from external dependencies
- Simulating API responses
- Testing time-dependent code
- Controlling random values
Key features:
- Mock functions with return values
- Module mocking for imports
- Timer mocking for async code
- HTTP mocking with MSW
Code Example#
Mock Functions#
1// Basic mock function
2import { vi, describe, it, expect } from 'vitest'
3
4const mockFn = vi.fn()
5mockFn('arg1', 'arg2')
6
7expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
8expect(mockFn).toHaveBeenCalledTimes(1)
9
10// Mock with return value
11const mockGet = vi.fn().mockReturnValue('result')
12const mockAsync = vi.fn().mockResolvedValue({ data: 'test' })
13
14expect(mockGet()).toBe('result')
15expect(await mockAsync()).toEqual({ data: 'test' })
16
17// Mock implementation
18const mockImpl = vi.fn().mockImplementation((x) => x * 2)
19expect(mockImpl(5)).toBe(10)
20
21// Mock return value once
22const mockOnce = vi.fn()
23 .mockReturnValueOnce('first')
24 .mockReturnValueOnce('second')
25 .mockReturnValue('default')
26
27expect(mockOnce()).toBe('first')
28expect(mockOnce()).toBe('second')
29expect(mockOnce()).toBe('default')Module Mocking#
1// __tests__/service.test.ts
2import { describe, it, expect, vi } from 'vitest'
3import { sendEmail } from '@/lib/email'
4import { createUser } from '@/services/user'
5
6// Mock entire module
7vi.mock('@/lib/email', () => ({
8 sendEmail: vi.fn().mockResolvedValue({ success: true })
9}))
10
11describe('createUser', () => {
12 it('sends welcome email', async () => {
13 await createUser({ email: 'test@example.com', name: 'Test' })
14
15 expect(sendEmail).toHaveBeenCalledWith(
16 expect.objectContaining({
17 to: 'test@example.com',
18 template: 'welcome'
19 })
20 )
21 })
22})
23
24// Partial Mocking - keep some real exports
25vi.mock('@/lib/utils', async () => {
26 const actual = await vi.importActual('@/lib/utils')
27 return {
28 ...actual,
29 generateId: vi.fn().mockReturnValue('test-id')
30 }
31})Spy on Methods#
1import { vi, describe, it, expect, afterEach } from 'vitest'
2
3describe('spying', () => {
4 afterEach(() => {
5 vi.restoreAllMocks()
6 })
7
8 it('spies without replacing', () => {
9 const consoleSpy = vi.spyOn(console, 'log')
10
11 myFunction() // calls console.log internally
12
13 expect(consoleSpy).toHaveBeenCalledWith('expected message')
14 })
15
16 it('spies and mocks', () => {
17 const dateSpy = vi.spyOn(Date, 'now').mockReturnValue(1234567890)
18
19 expect(Date.now()).toBe(1234567890)
20
21 dateSpy.mockRestore()
22 })
23
24 it('spies on object methods', () => {
25 const user = {
26 getName: () => 'John',
27 getAge: () => 30
28 }
29
30 const spy = vi.spyOn(user, 'getName').mockReturnValue('Jane')
31
32 expect(user.getName()).toBe('Jane')
33 expect(spy).toHaveBeenCalled()
34 })
35})Mock Timers#
1import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
2import { debounce } from '@/lib/utils'
3
4describe('debounce', () => {
5 beforeEach(() => {
6 vi.useFakeTimers()
7 })
8
9 afterEach(() => {
10 vi.useRealTimers()
11 })
12
13 it('debounces function calls', () => {
14 const fn = vi.fn()
15 const debounced = debounce(fn, 100)
16
17 debounced()
18 debounced()
19 debounced()
20
21 expect(fn).not.toHaveBeenCalled()
22
23 vi.advanceTimersByTime(100)
24
25 expect(fn).toHaveBeenCalledTimes(1)
26 })
27
28 it('uses fake Date', () => {
29 const mockDate = new Date('2024-01-15T12:00:00Z')
30 vi.setSystemTime(mockDate)
31
32 expect(new Date()).toEqual(mockDate)
33 })
34
35 it('runs all timers', () => {
36 const fn = vi.fn()
37 setTimeout(fn, 1000)
38 setTimeout(fn, 2000)
39
40 vi.runAllTimers()
41
42 expect(fn).toHaveBeenCalledTimes(2)
43 })
44})Mock Fetch/HTTP with MSW#
1// mocks/handlers.ts
2import { http, HttpResponse } from 'msw'
3
4export const handlers = [
5 http.get('/api/users', () => {
6 return HttpResponse.json([
7 { id: '1', name: 'Test User' }
8 ])
9 }),
10
11 http.post('/api/users', async ({ request }) => {
12 const body = await request.json()
13 return HttpResponse.json({ id: '2', ...body }, { status: 201 })
14 }),
15
16 http.get('/api/users/:id', ({ params }) => {
17 return HttpResponse.json({ id: params.id, name: 'User ' + params.id })
18 })
19]
20
21// mocks/server.ts
22import { setupServer } from 'msw/node'
23import { handlers } from './handlers'
24
25export const server = setupServer(...handlers)
26
27// vitest.setup.ts
28import { server } from './mocks/server'
29
30beforeAll(() => server.listen())
31afterEach(() => server.resetHandlers())
32afterAll(() => server.close())
33
34// In tests - override for specific test
35import { server } from '@/mocks/server'
36import { http, HttpResponse } from 'msw'
37
38it('handles error', async () => {
39 server.use(
40 http.get('/api/users', () => {
41 return HttpResponse.json({ error: 'Not found' }, { status: 404 })
42 })
43 )
44
45 // Test error handling...
46})Mock Prisma#
1// __mocks__/prisma.ts
2import { PrismaClient } from '@prisma/client'
3import { mockDeep, DeepMockProxy } from 'vitest-mock-extended'
4
5export type MockPrisma = DeepMockProxy<PrismaClient>
6
7export const prismaMock = mockDeep<PrismaClient>()
8
9vi.mock('@/lib/db', () => ({
10 prisma: prismaMock
11}))
12
13// In test file
14import { prismaMock } from '../__mocks__/prisma'
15
16describe('user service', () => {
17 beforeEach(() => {
18 vi.clearAllMocks()
19 })
20
21 it('finds user', async () => {
22 prismaMock.user.findUnique.mockResolvedValue({
23 id: '1',
24 email: 'test@example.com',
25 name: 'Test'
26 })
27
28 const user = await getUserById('1')
29 expect(user.email).toBe('test@example.com')
30 })
31
32 it('creates user', async () => {
33 prismaMock.user.create.mockResolvedValue({
34 id: '2',
35 email: 'new@example.com',
36 name: 'New User'
37 })
38
39 const user = await createUser({ email: 'new@example.com', name: 'New User' })
40 expect(user.id).toBe('2')
41 expect(prismaMock.user.create).toHaveBeenCalledWith({
42 data: expect.objectContaining({ email: 'new@example.com' })
43 })
44 })
45})Mock Next.js Modules#
1// vitest.setup.ts
2import { vi } from 'vitest'
3
4// Mock next/navigation
5vi.mock('next/navigation', () => ({
6 useRouter: () => ({
7 push: vi.fn(),
8 replace: vi.fn(),
9 back: vi.fn(),
10 prefetch: vi.fn()
11 }),
12 usePathname: () => '/',
13 useSearchParams: () => new URLSearchParams(),
14 useParams: () => ({})
15}))
16
17// Mock next/headers
18vi.mock('next/headers', () => ({
19 headers: () => new Map(),
20 cookies: () => ({
21 get: vi.fn(),
22 set: vi.fn(),
23 delete: vi.fn()
24 })
25}))
26
27// Mock next/cache
28vi.mock('next/cache', () => ({
29 revalidatePath: vi.fn(),
30 revalidateTag: vi.fn()
31}))
32
33// In specific test - override mock
34import { useRouter } from 'next/navigation'
35
36it('navigates on submit', async () => {
37 const push = vi.fn()
38 vi.mocked(useRouter).mockReturnValue({ push } as any)
39
40 // Test component that calls router.push
41 await submitForm()
42
43 expect(push).toHaveBeenCalledWith('/success')
44})Mock Environment Variables#
1import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
2
3describe('config', () => {
4 const originalEnv = process.env
5
6 beforeEach(() => {
7 vi.resetModules()
8 process.env = { ...originalEnv }
9 })
10
11 afterEach(() => {
12 process.env = originalEnv
13 })
14
15 it('uses production config', async () => {
16 process.env.NODE_ENV = 'production'
17 process.env.API_URL = 'https://api.prod.com'
18
19 const { config } = await import('@/lib/config')
20
21 expect(config.apiUrl).toBe('https://api.prod.com')
22 })
23
24 it('uses development config', async () => {
25 process.env.NODE_ENV = 'development'
26
27 const { config } = await import('@/lib/config')
28
29 expect(config.apiUrl).toBe('http://localhost:3000/api')
30 })
31})Usage Instructions#
- Choose mocking strategy: Decide between mock functions, module mocks, or MSW
- Set up mocks before tests: Use
vi.mock()at module level or inbeforeEach - Clear mocks between tests: Use
vi.clearAllMocks()inbeforeEach - Restore mocks after: Use
vi.restoreAllMocks()orvi.useRealTimers() - Override for specific tests: Use
server.use()for MSW or new mock implementations
Best Practices#
- Mock at the boundary - Mock external services, not internal logic
- Clear mocks between tests - Prevent state leakage
- Use MSW for HTTP - More realistic than mocking fetch directly
- Spy before mock - Sometimes spying is enough
- Keep mocks simple - Don't over-engineer mock implementations
- Document mock behavior - Make it clear what mocks return
- Test real code when possible - Only mock what you must
Related Patterns#
- Vitest - Test runner setup
- Unit Testing - Unit testing patterns
- Fixtures - Test data management
- Integration Testing - Integration tests