Vitest Mock 功能 #

Mock 概述 #

Mock(模拟)是单元测试中的核心概念,用于隔离测试单元,模拟外部依赖的行为。Vitest 提供了强大的 Mock 功能,通过 vi 对象提供各种 Mock 方法。

为什么需要 Mock? #

text
┌─────────────────────────────────────────────────────────────┐
│                    测试隔离的重要性                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  没有 Mock:                                                │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                │
│  │ 测试代码 │ -> │ 真实API │ -> │ 数据库   │                │
│  └─────────┘    └─────────┘    └─────────┘                │
│       │              │              │                      │
│       │         可能失败       可能失败                     │
│       │         网络问题       连接问题                     │
│       │         速度慢         数据污染                     │
│                                                             │
│  使用 Mock:                                                │
│  ┌─────────┐    ┌─────────┐                                │
│  │ 测试代码 │ -> │ Mock API │                               │
│  └─────────┘    └─────────┘                                │
│       │              │                                     │
│       │         可控、稳定                                  │
│       │         快速、隔离                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

vi 对象 #

vi 是 Vitest 的 Mock 工具对象:

typescript
import { vi } from 'vitest'

// vi 提供的方法
vi.fn()           // 创建 Mock 函数
vi.spyOn()        // 创建间谍函数
vi.mock()         // Mock 模块
vi.unmock()       // 取消 Mock
vi.doMock()       // 条件 Mock
vi.importActual() // 导入真实模块
vi.importMock()   // 导入 Mock 模块
vi.useFakeTimers() // 使用假定时器
vi.useRealTimers() // 使用真实定时器
vi.setSystemTime() // 设置系统时间
vi.mocked()       // 类型断言

函数 Mock #

vi.fn() 基本用法 #

创建一个 Mock 函数:

typescript
import { vi, expect, test } from 'vitest'

test('vi.fn() basic', () => {
  // 创建 Mock 函数
  const mockFn = vi.fn()
  
  // 调用函数
  mockFn('hello')
  mockFn('world')
  
  // 验证调用
  expect(mockFn).toHaveBeenCalled()
  expect(mockFn).toHaveBeenCalledTimes(2)
  expect(mockFn).toHaveBeenCalledWith('hello')
  expect(mockFn).toHaveBeenLastCalledWith('world')
})

设置返回值 #

typescript
import { vi, expect, test } from 'vitest'

test('mock return values', () => {
  // 固定返回值
  const mockFn1 = vi.fn().mockReturnValue(42)
  expect(mockFn1()).toBe(42)
  
  // 一次性返回值
  const mockFn2 = vi.fn()
    .mockReturnValueOnce(1)
    .mockReturnValueOnce(2)
    .mockReturnValue(3)
  
  expect(mockFn2()).toBe(1)
  expect(mockFn2()).toBe(2)
  expect(mockFn2()).toBe(3)
  expect(mockFn2()).toBe(3)
  
  // 返回 this
  const obj = { a: 1 }
  const mockFn3 = vi.fn().mockReturnThis()
  expect(mockFn3.call(obj)).toBe(obj)
})

设置实现 #

typescript
import { vi, expect, test } from 'vitest'

test('mock implementation', () => {
  // 自定义实现
  const mockFn1 = vi.fn((x: number) => x * 2)
  expect(mockFn1(5)).toBe(10)
  
  // 一次性实现
  const mockFn2 = vi.fn()
    .mockImplementationOnce((x: number) => x * 2)
    .mockImplementationOnce((x: number) => x * 3)
    .mockImplementation((x: number) => x * 4)
  
  expect(mockFn2(5)).toBe(10)
  expect(mockFn2(5)).toBe(15)
  expect(mockFn2(5)).toBe(20)
  expect(mockFn2(5)).toBe(20)
})

异步 Mock #

typescript
import { vi, expect, test } from 'vitest'

test('async mock', async () => {
  // 返回 Promise
  const mockFn1 = vi.fn().mockResolvedValue(42)
  expect(await mockFn1()).toBe(42)
  
  // 一次性 Promise
  const mockFn2 = vi.fn()
    .mockResolvedValueOnce(1)
    .mockResolvedValueOnce(2)
    .mockResolvedValue(3)
  
  expect(await mockFn2()).toBe(1)
  expect(await mockFn2()).toBe(2)
  expect(await mockFn2()).toBe(3)
  
  // 自定义异步实现
  const mockFn3 = vi.fn().mockImplementation(async (id: number) => {
    return { id, name: `User ${id}` }
  })
  
  const user = await mockFn3(1)
  expect(user).toEqual({ id: 1, name: 'User 1' })
})

Mock 函数属性 #

typescript
import { vi, expect, test } from 'vitest'

test('mock function properties', () => {
  const mockFn = vi.fn((x: number) => x * 2)
  
  mockFn(1)
  mockFn(2, 3)
  mockFn('hello')
  
  // 调用记录
  console.log(mockFn.mock.calls)
  // [[1], [2, 3], ['hello']]
  
  // 返回值记录
  console.log(mockFn.mock.results)
  // [{ type: 'return', value: 2 }, { type: 'return', value: 4 }, ...]
  
  // this 上下文
  console.log(mockFn.mock.contexts)
  
  // 调用实例
  console.log(mockFn.mock.instances)
})

间谍函数 #

vi.spyOn() 基本用法 #

监视对象方法的调用:

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

const obj = {
  getName: () => 'John',
  getAge: () => 30,
}

test('vi.spyOn() basic', () => {
  // 创建间谍
  const spy = vi.spyOn(obj, 'getName')
  
  // 调用方法
  const name = obj.getName()
  
  // 验证调用
  expect(spy).toHaveBeenCalled()
  expect(spy).toHaveReturnedWith('John')
  expect(name).toBe('John')
})

修改实现 #

typescript
import { vi, expect, test } from 'vitest'

test('spy implementation', () => {
  const obj = {
    getValue: () => 'original',
  }
  
  // 保留原始实现
  const spy1 = vi.spyOn(obj, 'getValue')
  expect(obj.getValue()).toBe('original')
  
  // 替换实现
  const spy2 = vi.spyOn(obj, 'getValue').mockReturnValue('mocked')
  expect(obj.getValue()).toBe('mocked')
  
  // 恢复原始实现
  spy2.mockRestore()
  expect(obj.getValue()).toBe('original')
})

间谍函数操作 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('spy operations', () => {
  const obj = {
    method: () => 'original',
  }
  let spy: ReturnType<typeof vi.spyOn>
  
  beforeEach(() => {
    spy = vi.spyOn(obj, 'method')
  })
  
  afterEach(() => {
    spy.mockRestore()  // 恢复原始实现
    // 或
    spy.mockReset()    // 重置调用记录
    // 或
    spy.mockClear()    // 清除调用记录
  })
  
  test('test 1', () => {
    obj.method()
    expect(spy).toHaveBeenCalled()
  })
  
  test('test 2', () => {
    obj.method()
    expect(spy).toHaveBeenCalled()
  })
})

spy 与 fn 的区别 #

typescript
import { vi, expect, test } from 'vitest'

test('spy vs fn', () => {
  const obj = {
    method: () => 'original',
  }
  
  // vi.fn() 创建新函数
  const mockFn = vi.fn()
  mockFn()
  expect(mockFn).toHaveBeenCalled()
  
  // vi.spyOn() 监视现有方法
  const spy = vi.spyOn(obj, 'method')
  obj.method()  // 调用原始方法
  expect(spy).toHaveBeenCalled()
  expect(obj.method()).toBe('original')  // 原始实现仍然工作
})

模块 Mock #

vi.mock() 基本用法 #

Mock 整个模块:

typescript
// api.ts
export async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// api.test.ts
import { vi, expect, test } from 'vitest'

// Mock 模块
vi.mock('./api', () => ({
  fetchUser: vi.fn(async (id: number) => ({
    id,
    name: `Mock User ${id}`,
  })),
}))

import { fetchUser } from './api'

test('mocked module', async () => {
  const user = await fetchUser(1)
  expect(user).toEqual({ id: 1, name: 'Mock User 1' })
})

自动 Mock #

typescript
import { vi, expect, test } from 'vitest'

// 自动 Mock,所有导出变为 vi.fn()
vi.mock('./api')

import { fetchUser, deleteUser } from './api'

test('auto mock', async () => {
  // 设置返回值
  vi.mocked(fetchUser).mockResolvedValue({ id: 1, name: 'Mock' })
  
  const user = await fetchUser(1)
  expect(user).toEqual({ id: 1, name: 'Mock' })
  
  // 验证调用
  expect(fetchUser).toHaveBeenCalledWith(1)
})

部分 Mock #

typescript
import { vi, expect, test } from 'vitest'

// 部分模块保持原始实现
vi.mock('./utils', async (importOriginal) => {
  const original = await importOriginal<typeof import('./utils')>()
  
  return {
    ...original,  // 保持其他导出
    // 只 Mock 特定函数
    formatDate: vi.fn((date: Date) => 'mocked date'),
  }
})

import { formatDate, calculateSum } from './utils'

test('partial mock', () => {
  expect(formatDate(new Date())).toBe('mocked date')
  expect(calculateSum(1, 2)).toBe(3)  // 原始实现
})

vi.importActual() #

导入真实模块:

typescript
import { vi, expect, test } from 'vitest'

vi.mock('./utils', async () => {
  const actual = await vi.importActual<typeof import('./utils')>('./utils')
  
  return {
    ...actual,
    // 只 Mock 特定函数
    fetchData: vi.fn().mockResolvedValue({ data: 'mocked' }),
  }
})

vi.importMock() #

导入 Mock 模块:

typescript
import { vi, expect, test } from 'vitest'

test('import mock', async () => {
  const mockedModule = await vi.importMock<typeof import('./api')>('./api')
  
  // mockedModule 所有导出都是 vi.fn()
  mockedModule.fetchUser.mockResolvedValue({ id: 1 })
})

vi.doMock() #

条件 Mock:

typescript
import { vi, expect, test, beforeEach } from 'vitest'

beforeEach(() => {
  vi.doMock('./config', () => ({
    API_URL: 'http://test-api.com',
  }))
})

test('conditional mock', async () => {
  // 在测试内部导入
  const { API_URL } = await import('./config')
  expect(API_URL).toBe('http://test-api.com')
})

vi.unmock() #

取消 Mock:

typescript
import { vi, expect, test } from 'vitest'

vi.mock('./api')

test('mocked', async () => {
  const { fetchUser } = await import('./api')
  // fetchUser 是 Mock 函数
})

vi.unmock('./api')

test('unmocked', async () => {
  const { fetchUser } = await import('./api')
  // fetchUser 是真实函数
})

定时器 Mock #

vi.useFakeTimers() #

使用假定时器:

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

test('setTimeout', () => {
  const callback = vi.fn()
  
  setTimeout(callback, 1000)
  
  // 时间未到,未调用
  expect(callback).not.toHaveBeenCalled()
  
  // 快进时间
  vi.advanceTimersByTime(1000)
  
  // 已调用
  expect(callback).toHaveBeenCalled()
})

test('setInterval', () => {
  const callback = vi.fn()
  
  setInterval(callback, 1000)
  
  // 快进 3 秒
  vi.advanceTimersByTime(3000)
  
  // 调用 3 次
  expect(callback).toHaveBeenCalledTimes(3)
})

控制时间 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

test('time control', () => {
  const callback = vi.fn()
  
  setTimeout(callback, 1000)
  setTimeout(callback, 2000)
  setTimeout(callback, 3000)
  
  // 快进到下一个定时器
  vi.advanceTimersToNextTimer()
  expect(callback).toHaveBeenCalledTimes(1)
  
  // 快进到下一个定时器
  vi.advanceTimersToNextTimer()
  expect(callback).toHaveBeenCalledTimes(2)
  
  // 快进到下一个定时器
  vi.advanceTimersToNextTimer()
  expect(callback).toHaveBeenCalledTimes(3)
})

test('run all timers', () => {
  const callback = vi.fn()
  
  setTimeout(callback, 1000)
  setTimeout(callback, 2000)
  setTimeout(callback, 3000)
  
  // 运行所有定时器
  vi.runAllTimers()
  
  expect(callback).toHaveBeenCalledTimes(3)
})

test('run only pending timers', () => {
  const callback = vi.fn()
  
  setTimeout(callback, 1000)
  
  // 只运行待处理的定时器
  vi.runOnlyPendingTimers()
  
  expect(callback).toHaveBeenCalledTimes(1)
})

设置系统时间 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

test('set system time', () => {
  // 设置系统时间
  const now = new Date('2024-01-15T10:00:00')
  vi.setSystemTime(now)
  
  // Date.now() 返回设置的时间
  expect(Date.now()).toBe(now.getTime())
  
  // new Date() 返回设置的时间
  expect(new Date()).toEqual(now)
})

定时器 Mock 配置 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

beforeEach(() => {
  vi.useFakeTimers({
    // 只 Mock 特定定时器
    toFake: ['setTimeout', 'setInterval'],
    
    // 设置初始时间
    now: new Date('2024-01-01'),
    
    // 循环上限
    loopLimit: 1000,
  })
})

afterEach(() => {
  vi.useRealTimers()
})

全局 Mock #

Mock 全局变量 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('global mock', () => {
  const originalFetch = global.fetch
  
  beforeEach(() => {
    global.fetch = vi.fn()
  })
  
  afterEach(() => {
    global.fetch = originalFetch
  })
  
  test('mock fetch', async () => {
    vi.mocked(fetch).mockResolvedValue({
      ok: true,
      json: async () => ({ data: 'mocked' }),
    } as Response)
    
    const response = await fetch('/api/data')
    const data = await response.json()
    
    expect(data).toEqual({ data: 'mocked' })
  })
})

Mock localStorage #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('localStorage mock', () => {
  const localStorageMock = (() => {
    let store: Record<string, string> = {}
    return {
      getItem: (key: string) => store[key] || null,
      setItem: (key: string, value: string) => {
        store[key] = value
      },
      removeItem: (key: string) => {
        delete store[key]
      },
      clear: () => {
        store = {}
      },
    }
  })()
  
  beforeEach(() => {
    Object.defineProperty(global, 'localStorage', {
      value: localStorageMock,
    })
  })
  
  test('localStorage', () => {
    localStorage.setItem('key', 'value')
    expect(localStorage.getItem('key')).toBe('value')
  })
})

Mock console #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('console mock', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>
  
  beforeEach(() => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
  })
  
  afterEach(() => {
    consoleSpy.mockRestore()
  })
  
  test('console.log', () => {
    console.log('test message')
    
    expect(consoleSpy).toHaveBeenCalledWith('test message')
  })
})

Mock 最佳实践 #

1. 在 beforeEach 中设置 Mock #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'
import { fetchUser } from './api'

vi.mock('./api')

describe('User tests', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(fetchUser).mockResolvedValue({ id: 1, name: 'Test' })
  })
  
  afterEach(() => {
    vi.restoreAllMocks()
  })
  
  test('test 1', async () => {
    await fetchUser(1)
    expect(fetchUser).toHaveBeenCalledWith(1)
  })
  
  test('test 2', async () => {
    await fetchUser(2)
    expect(fetchUser).toHaveBeenCalledWith(2)
  })
})

2. 使用 vi.mocked() 类型断言 #

typescript
import { vi, expect, test } from 'vitest'
import { fetchUser } from './api'

vi.mock('./api')

test('typed mock', async () => {
  // 类型安全的 Mock
  vi.mocked(fetchUser).mockResolvedValue({ id: 1, name: 'Test' })
  
  const user = await fetchUser(1)
  expect(user.name).toBe('Test')
})

3. 隔离测试 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('isolated tests', () => {
  beforeEach(() => {
    vi.resetModules()  // 重置模块缓存
  })
  
  afterEach(() => {
    vi.restoreAllMocks()
  })
  
  test('test 1', async () => {
    vi.doMock('./config', () => ({ value: 1 }))
    const { value } = await import('./config')
    expect(value).toBe(1)
  })
  
  test('test 2', async () => {
    vi.doMock('./config', () => ({ value: 2 }))
    const { value } = await import('./config')
    expect(value).toBe(2)
  })
})

4. Mock 清理 #

typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'

describe('mock cleanup', () => {
  beforeEach(() => {
    vi.clearAllMocks()    // 清除调用记录
    // 或
    vi.resetAllMocks()    // 重置 Mock 实现
    // 或
    vi.restoreAllMocks()  // 恢复原始实现
  })
  
  test('clean test', () => {
    const mockFn = vi.fn()
    mockFn()
    expect(mockFn).toHaveBeenCalled()
  })
})

Mock 速查表 #

方法 说明
vi.fn(impl?) 创建 Mock 函数
vi.spyOn(obj, method) 创建间谍函数
vi.mock(path, factory?) Mock 模块
vi.unmock(path) 取消 Mock
vi.doMock(path, factory?) 条件 Mock
vi.importActual(path) 导入真实模块
vi.importMock(path) 导入 Mock 模块
vi.useFakeTimers() 使用假定时器
vi.useRealTimers() 使用真实定时器
vi.setSystemTime(time) 设置系统时间
vi.mocked(fn) 类型断言
vi.clearAllMocks() 清除所有调用记录
vi.resetAllMocks() 重置所有 Mock
vi.restoreAllMocks() 恢复所有原始实现
vi.resetModules() 重置模块缓存

下一步 #

现在你已经掌握了 Mock 功能,接下来学习 快照测试 了解如何使用快照测试 UI 组件!

最后更新:2026-03-28