第6章 Mocking(模拟测试)指南与速查表
Mocking
TIP
如果你是 mocking 新手,可以从 Mock Functions 教程入手,通过实践了解 vi.fn、vi.spyOn 和 vi.mock。
编写测试时,迟早需要创建内部或外部服务的“伪”版本,这就是常说的 mocking。Vitest 通过 vi 辅助对象提供了一系列实用函数来帮你实现。你可以从 vitest 导入它;若已开启全局配置,也可在全局直接访问。
WARNING
务必记得在每次测试运行前后清除或恢复 mocks,以免上一轮的状态变化影响下一轮。详见 mockReset 文档。
如果你还不太熟悉 vi.fn、vi.mock 或 vi.spyOn 方法,请先查看 API 部分。
Vitest 提供了关于 mocking 的全面指南:
Mocking Classes
Mocking Dates
Mocking the File System
Mocking Functions
Mocking Globals
Mocking Modules
Mocking Requests
Mocking Timers
如果你想要更简单、快速的上手方式,可以查看下方的 Cheat Sheet。
Cheat Sheet
I want to…
Mock 导出的变量
example.js
export const getter = 'variable'
example.test.ts
import * as exports from './example.js'
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked')
WARNING
此写法在 Browser Mode 下无法生效。替代方案见 Limitations。
Mock 导出的函数
示例(使用 vi.mock):
WARNING
注意 vi.mock 调用会被提升到文件顶部。它总是先于所有 import 执行。
example.js
export function method() {}
ts
import { method } from './example.js'
vi.mock('./example.js', () => ({ method: vi.fn() }))
示例(使用 vi.spyOn):
ts
import * as exports from './example.js'
vi.spyOn(exports, 'method').mockImplementation(() => {})
WARNING
vi.spyOn 示例在 Browser Mode 下无法生效。替代方案见 Limitations。
Mock 导出类的实现
示例(使用伪造类):
example.js
export class SomeClass {}
ts
import { SomeClass } from './example.js'
vi.mock(import('./example.js'), () => { const SomeClass = vi.fn(class FakeClass { someMethod = vi.fn() }) return { SomeClass } })
示例(使用 vi.spyOn):
ts
import * as mod from './example.js'
vi.spyOn(mod, 'SomeClass').mockImplementation(class FakeClass { someMethod = vi.fn() })
WARNING
vi.spyOn 示例在 Browser Mode 下无法生效。替代方案见 Limitations。
Spy 函数返回的对象
示例(使用缓存):
example.js
export function useObject() { return { method: () => true } }
useObject.js
import { useObject } from './example.js'
const obj = useObject() obj.method()
useObject.test.js
import { useObject } from './example.js'
vi.mock(import('./example.js'), () => { let _cache const useObject = () => { if (!_cache) { _cache = { method: vi.fn(), } } // 此后每次调用 useObject() 都会返回同一个对象引用 return _cache } return { useObject } })
const obj = useObject() // obj.method 曾在 some-path 中被调用 expect(obj.method).toHaveBeenCalled()
Mock 模块的一部分
ts
import { mocked, original } from './some-path.js'
vi.mock(import('./some-path.js'), async (importOriginal) => { const mod = await importOriginal() return { ...mod, mocked: vi.fn() } })
original() // 保留原有行为 mocked() // 是一个 spy 函数
WARNING
请注意,这里只 mock 了外部访问。在本例中,若 original 内部调用了 mocked,则永远会调用模块内定义的函数,而非 mock 工厂里的实现。
Mock 当前日期
Mock Date 和 Temporal 时间可以使用 vi.setSystemTime 辅助函数。该值不会在不同测试之间自动重置。
注意:使用 vi.useFakeTimers 也会改变 Date 的时间。
ts
const mockDate = new Date(2022, 0, 1) vi.setSystemTime(mockDate) const now = new Date() expect(now.valueOf()).toBe(mockDate.valueOf()) const nowInstant = Temporal.Now.instant() expect(nowInstant.epochMilliseconds).toBe(mockDate.valueOf()) // 重置被 mock 的时间 vi.useRealTimers()
Mock 全局变量
你可以通过给 globalThis 赋值来设置全局变量,也可以使用 vi.stubGlobal 辅助函数。使用 vi.stubGlobal 时,值不会在不同测试之间自动重置,除非你启用 unstubGlobals 配置选项,或手动调用 vi.unstubAllGlobals。
ts
vi.stubGlobal('__VERSION__', '1.0.0') expect(__VERSION__).toBe('1.0.0')
Mock import.meta.env
若要修改环境变量,直接给它赋新值即可。
WARNING
环境变量的值不会在不同测试之间自动重置。
ts
import { beforeEach, expect, it } from 'vitest'
// 你可以手动在 beforeEach 钩子中重置它 const originalViteEnv = import.meta.env.VITE_ENV
beforeEach(() => { import.meta.env.VITE_ENV = originalViteEnv })
it('changes value', () => { import.meta.env.VITE_ENV = 'staging' expect(import.meta.env.VITE_ENV).toBe('staging') })
如果你希望自动重置值,可以在开启 unstubEnvs 配置选项的情况下使用 vi.stubEnv 辅助函数(或在 beforeEach 钩子中手动调用 vi.unstubAllEnvs):
ts
import { expect, it, vi } from 'vitest'
// 运行测试前 "VITE_ENV" 为 "test" import.meta.env.VITE_ENV === 'test'
it('changes value', () => { vi.stubEnv('VITE_ENV', 'staging') expect(import.meta.env.VITE_ENV).toBe('staging') })
it('the value is restored before running an other test', () => { expect(import.meta.env.VITE_ENV).toBe('test') })
vitest.config.ts
export default defineConfig({ test: { unstubEnvs: true, }, })