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#

  1. Choose mocking strategy: Decide between mock functions, module mocks, or MSW
  2. Set up mocks before tests: Use vi.mock() at module level or in beforeEach
  3. Clear mocks between tests: Use vi.clearAllMocks() in beforeEach
  4. Restore mocks after: Use vi.restoreAllMocks() or vi.useRealTimers()
  5. Override for specific tests: Use server.use() for MSW or new mock implementations

Best Practices#

  1. Mock at the boundary - Mock external services, not internal logic
  2. Clear mocks between tests - Prevent state leakage
  3. Use MSW for HTTP - More realistic than mocking fetch directly
  4. Spy before mock - Sometimes spying is enough
  5. Keep mocks simple - Don't over-engineer mock implementations
  6. Document mock behavior - Make it clear what mocks return
  7. Test real code when possible - Only mock what you must