ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vitest Mocks API 完全指南:掌握 vi.fn 与 vi.spyOn 的 Mock 行为控制

Vitest Mocks API 完全指南:掌握 vi.fn 与 vi.spyOn 的 Mock 行为控制 Vitest Mocks API 完全指南掌握 vi.fn 与 vi.spyOn 的 Mock 行为控制【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest本指南以 Vitest 官方 API 参考文档 docs/api/mock.md 为核心骨架系统讲解 Mock 对象的全部方法与状态属性从vi.fn()/vi.spyOn()的创建、实现替换Implementation、返回值/异常/异步值注入到mock.calls、mock.results等调用记录结构的深入理解并结合仓库源码packages/spy/src/index.ts与测试用例test/unit/test/mocking/vi-fn.test.ts揭示底层实现原理。读完本文你将掌握如何在测试中精准追踪、操控和断言 Mock 行为写出可复现、可维护的高质量测试。一、Mocks 概览跟踪函数执行与对象属性在 Vitest 中你可以通过vi.fn创建一个 Mock 函数或 Mock 类来跟踪其执行情况如果需要跟踪一个已存在对象上的属性则使用vi.spyOn方法import { vi } from vitest const fn vi.fn() fn(hello world) fn.mock.calls[0] [hello world] const market { getApples: () 100 } const getApplesSpy vi.spyOn(market, getApples) market.getApples() getApplesSpy.mock.calls.length 1断言 Mock 结果时应当使用 expect 上的 Mock 断言方法如toHaveBeenCalled。本文 API 参考文档描述了可用于操控 Mock 行为的全部属性和方法。从源码看vi.fn与vi.spyOn最终都汇聚到 packages/spy/src/index.ts 中的createMockInstance工厂函数fnpackages/spy/src/index.ts直接以原函数作为mockImplementation创建实例spyOnpackages/spy/src/index.ts则先通过getDescriptor沿原型链查找属性描述符再用Object.defineProperty将目标对象上的方法替换为 Mock同时保存restore回调以便恢复。1.1 重要length属性的继承规则Vitest 的 spies 在初始化时会继承实现的length属性但后续修改实现不会覆盖它::: code-groupconst fn vi.fn((arg1) {}) fn.length // 1 fn.mockImplementation(() {}) fn.length // 1const example { fn(arg1, arg2) { // ... } } const fn vi.spyOn(example, fn) fn.length // 2 fn.mockImplementation(() {}) fn.length // 2:::底层实现位于 packages/spy/src/index.ts创建 Mock 时通过Object.defineProperty(mock, length, { writable: true, ... })固化初始length取mockImplementation || originalImplementation的长度因此length是可写的但默认不会随实现变化而自动更新。对应测试见 test/unit/test/mocking/vi-fn.test.tsfn.length is consistent 与 vi.fn() has overridable length 两个用例。1.2 类支持Class Support简写方法如mockReturnValue、mockReturnValueOnce、mockResolvedValue等不能用于被 Mock 的类。类的构造函数在返回值方面存在反直觉行为const CorrectDogClass vi.fn(class { constructor(public name: string) {} }) const IncorrectDogClass vi.fn(class { constructor(public name: string) { return { name } } }) const Marti new CorrectDogClass(Marti) const Newt new IncorrectDogClass(Newt) Marti instanceof CorrectDogClass // ✅ true Newt instanceof IncorrectDogClass // ❌ false!虽然两者形状相同但构造函数返回的return value被赋给了Newt——它是一个普通对象而不是 Mock 的实例。Vitest 会在简写方法但不是mockImplementation中对此行为做防护并抛出错误。如果需要 Mock 构造出的类实例建议改用class语法配合mockImplementationmock.mockReturnValue({ hello: () world }) // [!code --] mock.mockImplementation(class { hello () world }) // [!code ]如果你确实需要测试构造函数返回对象这一合法场景可以直接在mockImplementation中使用constructormock.mockImplementation(class { constructor(name: string) { return { name } } })这一防护机制在源码中有迹可循mockReturnValue、mockResolvedValue、mockRejectedValue及其 Once 变体在执行时都会检查new.target一旦以new调用便触发throwConstructorErrorpackages/spy/src/index.ts错误信息指向 Class Support 文档说明packages/spy/src/index.ts。二、获取与标识 MockgetMockImplementation / getMockName / mockNamegetMockImplementationfunction getMockImplementation(): T | undefined返回当前 Mock 的实现如果存在。如果 Mock 由vi.fn创建它会返回传入的方法作为实现如果 Mock 由vi.spyOn创建除非提供了自定义实现否则返回undefined。注意源码中的一个实现细节getMockImplementation返回的是config.onceMockImplementations[0] || config.mockImplementationpackages/spy/src/index.ts即下一次调用将使用哪个实现而不是笼统的当前实现——这保证了一次性实现Once尚未被消费时也能被正确读取。getMockNamefunction getMockName(): string返回通过.mockName(name)方法分配给 Mock 的名称。默认情况下vi.fn()创建的 Mock 返回vi.fn()而vi.spyOn创建的 spies 保留原始名称。源码中getMockName同样以vi.fn()作为兜底默认值packages/spy/src/index.ts。mockNamefunction mockName(name: string): MockT设置内部 Mock 名称。当断言失败时这个名称有助于快速定位是哪个 Mock 出了问题。它同时影响快照与日志输出见 packages/spy/src/index.ts 中resetToMockName的处理spyOn 会继承原函数名以便调试。三、实现控制mockImplementation 与 mockImplementationOncemockImplementationfunction mockImplementation(fn: T): MockT接受一个函数作为 Mock 的实现。TypeScript 要求该函数的参数与返回值类型和原函数匹配。const mockFn vi.fn().mockImplementation((apples: number) apples 1) // or: vi.fn(apples apples 1); const NelliesBucket mockFn(0) const BobsBucket mockFn(1) NelliesBucket 1 // true BobsBucket 2 // true mockFn.mock.calls[0][0] 0 // true mockFn.mock.calls[1][0] 1 // true如果实现是一个类Mock 的prototype会重新指向该实现的prototype因此构造出的实例可以看到其原型方法并且能通过针对它的instanceof检查。详见 Mocking Classes对应文档 docs/guide/mocking/classes.md。源码中mockImplementation在设置config.mockImplementation后立即调用updateMockPrototype()packages/spy/src/index.ts而updateMockPrototype通过reparentMockPrototypepackages/spy/src/index.ts把mock.prototype的原型链挂到当前实现上——这就是构造实例能看到原型方法的机制来源。mockImplementationOncefunction mockImplementationOnce(fn: T): MockT接受一个函数作为 Mock 的实现。TypeScript 要求参数与返回值类型和原函数匹配。该方法可以链式调用为多次函数调用产生不同的结果const myMockFn vi .fn() .mockImplementationOnce(() true) // 1st call .mockImplementationOnce(() false) // 2nd call myMockFn() // 1st call: true myMockFn() // 2nd call: false当一次性实现被消耗完后Mock 会回退到默认实现即通过vi.fn(() defaultValue)或.mockImplementation(() defaultValue)设置的实现const myMockFn vi .fn(() default) .mockImplementationOnce(() first call) .mockImplementationOnce(() second call) // first call, second call, default, default console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn())从源码可以看到 Once 实现的消费顺序每次调用时config.onceMockImplementations.shift()packages/spy/src/index.ts取队首的一次性实现取不到才回退到config.mockImplementation最后是原始实现或空函数。withImplementationfunction withImplementation( fn: T, cb: () void ): MockT function withImplementation( fn: T, cb: () Promisevoid ): PromiseMockT在回调执行期间临时覆盖原始 Mock 实现const myMockFn vi.fn(() original) myMockFn.withImplementation(() temp, () { myMockFn() // temp }) myMockFn() // original也可以配合异步回调使用。此时必须await该方法之后才能使用原始实现test(async callback, () { const myMockFn vi.fn(() original) // We await this call since the callback is async await myMockFn.withImplementation( () temp, async () { myMockFn() // temp }, ) myMockFn() // original })注意withImplementation的优先级高于mockImplementationOnce。源码实现packages/spy/src/index.ts会暂存并清空当前实现与 Once 队列执行回调然后无论回调是同步还是 Promise 都在结束时恢复原状。四、同步返回值mockReturnValue / mockReturnValueOnce / mockReturnThis / mockThrow / mockThrowOncemockReturnValuefunction mockReturnValue(value: ReturnTypeT): MockT接受一个值Mock 函数每次被调用时都会返回它。TypeScript 只接受与原函数返回类型匹配的值const mock vi.fn() mock.mockReturnValue(42) mock() // 42 mock.mockReturnValue(43) mock() // 43mockReturnValueOncefunction mockReturnValueOnce(value: ReturnTypeT): MockT接受一个值在下一次函数调用时返回。TypeScript 只接受与原函数返回类型匹配的值。可以链式调用每次连续调用依次返回指定值const myMockFn vi .fn() .mockReturnValue(default) .mockReturnValueOnce(first call) .mockReturnValueOnce(second call) // first call, second call, default, default console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn())与mockImplementationOnce类似Once 版本耗尽后回退到默认实现。mockReturnThisfunction mockReturnThis(): MockT当你需要让方法返回this上下文且不调用真实实现时使用。它是以下写法的简写spy.mockImplementation(function () { return this })源码中mockReturnThis正是通过mockImplementation(function () { return this })实现的packages/spy/src/index.ts非常适合链式调用风格的 API 测试。mockThrow4.1.0function mockThrow(value: unknown): MockT接受一个值Mock 函数每次被调用时都会抛出它const myMockFn vi.fn() myMockFn.mockThrow(new Error(error message)) myMockFn() // throws Errorerror messagemockThrowOnce4.1.0function mockThrowOnce(value: unknown): MockT接受一个值在下一次函数调用时抛出。链式调用时每次连续调用依次抛出指定值const myMockFn vi .fn() .mockReturnValue(default) .mockThrowOnce(new Error(first call error)) .mockThrowOnce(second call error) expect(() myMockFn()).toThrow(first call error) expect(() myMockFn()).toThrow(second call error) expect(myMockFn()).toEqual(default)源码中mockThrow/mockThrowOnce分别以throw value的实现注册到mockImplementation/mockImplementationOncepackages/spy/src/index.ts因此它们同样遵守Once 优先、默认实现兜底的消费规则。五、异步行为mockResolvedValue / mockResolvedValueOnce / mockRejectedValue / mockRejectedValueOncemockResolvedValuefunction mockResolvedValue(value: AwaitedReturnTypeT): MockT接受一个值异步函数被调用时将以该值 resolve。TypeScript 只接受与原函数返回类型匹配的值const asyncMock vi.fn().mockResolvedValue(42) await asyncMock() // 42mockResolvedValueOncefunction mockResolvedValueOnce(value: AwaitedReturnTypeT): MockT接受一个值在下一次函数调用时 resolve。TypeScript 只接受与原函数返回类型匹配的值。链式调用时每次连续调用依次 resolve 指定值const asyncMock vi .fn() .mockResolvedValue(default) .mockResolvedValueOnce(first call) .mockResolvedValueOnce(second call) await asyncMock() // first call await asyncMock() // second call await asyncMock() // default await asyncMock() // defaultmockRejectedValuefunction mockRejectedValue(value: unknown): MockT接受一个错误异步函数被调用时将以该错误 rejectconst asyncMock vi.fn().mockRejectedValue(new Error(Async error)) await asyncMock() // throws ErrorAsync errormockRejectedValueOncefunction mockRejectedValueOnce(value: unknown): MockT接受一个值在下一次函数调用时 reject。链式调用时每次连续调用依次 reject 指定值const asyncMock vi .fn() .mockResolvedValueOnce(first call) .mockRejectedValueOnce(new Error(Async error)) await asyncMock() // first call await asyncMock() // throws ErrorAsync error源码中这四个方法都是通过注册一个返回Promise.resolve(value)/Promise.reject(value)的实现来实现的packages/spy/src/index.ts并且与简写方法一样带有new.target构造函数防护。六、清理与恢复mockClear / mockReset / mockRestoremockClearfunction mockClear(): MockT清空关于每次调用的全部信息。调用后.mock上的所有属性都会回到初始状态。该方法不会重置实现适合在不同断言之间清理 Mock。const person { greet: (name: string) Hello ${name}, } const spy vi.spyOn(person, greet).mockImplementation(() mocked) expect(person.greet(Alice)).toBe(mocked) expect(spy.mock.calls).toEqual([[Alice]]) // clear call history but keep mock implementation spy.mockClear() expect(spy.mock.calls).toEqual([]) expect(person.greet(Bob)).toBe(mocked) expect(spy.mock.calls).toEqual([[Bob]])如需在每个测试前自动调用该方法可在配置中启用clearMocks设置。源码中mockClear会清空calls、contexts、instances、invocationCallOrder、results、settledResults六个数组packages/spy/src/index.ts与getDefaultStatepackages/spy/src/index.ts定义的初始状态一一对应。mockResetfunction mockReset(): MockT完成mockClear所做的一切并重置 Mock 实现同时清空所有Once实现。重置vi.fn()创建的 Mock 会将其实现设置为返回undefined的空函数重置vi.fn(impl)创建的 Mock 会将实现恢复为impl。Mock 的prototype链也会随之变化vi.fn(impl)与vi.spyOn()恢复到原始类vi.fn()恢复到普通对象因此在重置后构造的实例不再能通过先前类实现的instanceof检查。当你需要把 Mock 恢复到原始状态时使用const person { greet: (name: string) Hello ${name}, } const spy vi.spyOn(person, greet).mockImplementation(() mocked) expect(person.greet(Alice)).toBe(mocked) expect(spy.mock.calls).toEqual([[Alice]]) // clear call history and reset implementation, but method is still spied spy.mockReset() expect(spy.mock.calls).toEqual([]) expect(person.greet).toBe(spy) expect(person.greet(Bob)).toBe(Hello Bob) expect(spy.mock.calls).toEqual([[Bob]])如需在每个测试前自动调用启用mockReset配置。源码实现packages/spy/src/index.ts在mockClear基础上依据resetToMockImplementation决定恢复为vi.fn(impl)传入的原实现还是undefined并清空onceMockImplementations、重置 mockName。mockRestorefunction mockRestore(): MockT完成mockReset所做的一切并且恢复被 spy 对象的原始描述符如果 Mock 由vi.spyOn创建。对vi.fn()创建的 Mock 调用mockRestore与mockReset完全一致const person { greet: (name: string) Hello ${name}, } const spy vi.spyOn(person, greet).mockImplementation(() mocked) expect(person.greet(Alice)).toBe(mocked) expect(spy.mock.calls).toEqual([[Alice]]) // clear call history and restore spied object method spy.mockRestore() expect(spy.mock.calls).toEqual([]) expect(person.greet).not.toBe(spy) expect(person.greet(Bob)).toBe(Hello Bob) expect(spy.mock.calls).toEqual([])如需在每个测试前自动调用启用restoreMocks配置。源码中mockRestore在mockReset之后调用创建 spyOn 时保存的restore回调packages/spy/src/index.ts该回调packages/spy/src/index.ts会判断属性是定义在原型上Reflect.deleteProperty删除自身覆盖还是对象自身上Object.defineProperty恢复原始描述符。相关配置默认值以上三个配置项在 packages/vitest/src/defaults.ts 中的默认值分别为clearMocks: true、mockReset: false、restoreMocks: false。也就是说Vitest 默认在每个测试前自动执行mockClear清理调用记录但保留实现而是否重置实现与是否恢复原始描述符需要按需开启。此外vi.clearAllMocks()/vi.resetAllMocks()/vi.restoreAllMocks()在全局层面的实现也位于 packages/spy/src/index.ts其中clearAllMocks只清理被调用过的脏状态 MockDIRTY_MOCK_STATESresetAllMocks则通过WeakRefFinalizationRegistry遍历所有已注册 Mock兼顾内存回收。七、状态记录Mock Statecalls / lastCall / results / settledResults / invocationCallOrder / contexts / instances所有调用信息都挂在mock对象上其类型定义见 packages/spy/src/types.ts 中的MockContext接口初始结构由getDefaultState创建packages/spy/src/index.ts。mock.callsconst calls: ParametersT[]一个包含每次调用全部参数的数组。数组中的每一项就是那一次调用的参数const fn vi.fn() fn(arg1, arg2) fn(arg3) fn.mock.calls [ [arg1, arg2], // first call [arg3], // second call ]:::warning 对象按引用存储 注意 Vitest 在mock状态的所有属性中始终按引用存储对象。这意味着如果你的代码修改了这些属性某些断言如.toHaveBeenCalledWith将无法通过const argument { value: 0, } const fn vi.fn() fn(argument) // { value: 0 } argument.value 10 expect(fn).toHaveBeenCalledWith({ value: 0 }) // [!code --] // The equality check is done against the original argument, // but its property was changed between the call and assertion expect(fn).toHaveBeenCalledWith({ value: 10 }) // [!code ]这种情况下可以自行克隆参数const calledArguments [] const fn vi.fn((arg) { calledArguments.push(structuredClone(arg)) }) expect(calledArguments[0]).toEqual({ value: 0 }):::mock.lastCallconst lastCall: ParametersT | undefined包含最后一次调用的参数。如果 Mock 从未被调用返回undefined。源码中它是calls数组的 getterstate.calls.at(-1)见 packages/spy/src/index.ts因此始终与calls保持同步。mock.resultsinterface MockResultReturnT { type: return /** * The value that was returned from the function. * If the function returned a Promise, then this will be a resolved value. */ value: T } interface MockResultIncomplete { type: incomplete value: undefined } interface MockResultThrow { type: throw /** * An error that was thrown during function execution. */ value: any } type MockResultT | MockResultReturnT | MockResultThrow | MockResultIncomplete const results: MockResultReturnTypeT[]一个包含函数return返回的所有值的数组。数组中的每一项是包含type和value属性的对象。可用类型return— 函数正常返回未抛出异常throw— 函数抛出了值incomplete— 函数尚未执行完毕。value属性包含返回值或抛出的错误。如果函数返回的是Promise即使该 promise 被 rejectresult也永远是returnconst fn vi.fn() .mockReturnValueOnce(result) .mockImplementationOnce(() { throw new Error(thrown error) }) const result fn() // returned result try { fn() // threw Error } catch {} fn.mock.results [ // first result { type: return, value: result, }, // last result { type: throw, value: Error, }, ]mock.settledResultsinterface MockSettledResultIncomplete { type: incomplete value: undefined } interface MockSettledResultFulfilledT { type: fulfilled value: T } interface MockSettledResultRejected { type: rejected value: any } export type MockSettledResultT | MockSettledResultFulfilledT | MockSettledResultRejected | MockSettledResultIncomplete const settledResults: MockSettledResultAwaitedReturnTypeT[]一个包含函数 resolve 或 reject 的所有值的数组。如果函数返回的是非 Promise 值value保持原样但type仍然会标明fulfilled或rejected。在值被 resolve 或 reject 之前settledResult的类型为incompleteconst fn vi.fn().mockResolvedValueOnce(result) const result fn() fn.mock.settledResults [ { type: incomplete, value: undefined, }, ] await result fn.mock.settledResults [ { type: fulfilled, value: result, }, ]从源码看results与settledResults在调用时先登记为incompletepackages/spy/src/index.ts同步抛出/返回则在finally中更新为throw/return若返回值是Promise则通过returnValue.then(...)在微任务中把settledResult更新为fulfilled或rejectedpackages/spy/src/index.ts——这正是results对 Promise 恒为return、而settledResults能反映真实落定状态的原因。mock.invocationCallOrderconst invocationCallOrder: number[]返回 Mock 函数执行的顺序。它是一个数字数组这些数字在所有已定义的 Mocks 之间共享const fn1 vi.fn() const fn2 vi.fn() fn1() fn2() fn1() fn1.mock.invocationCallOrder [1, 3] fn2.mock.invocationCallOrder [2]实现上使用模块级计数器invocationCallCounterpackages/spy/src/index.ts每次调用registerInvocationOrder(invocationCallCounter, state, ...)递增并记录因此它是跨所有 Mock 的全局有序序列可用于验证跨 Mock 的调用先后关系与 expect 的toHaveBeenCalledBefore/toHaveBeenCalledAfter断言语义一致。mock.contextsconst contexts: ThisParameterTypeT[]一个包含每次调用 Mock 函数时使用的this值的数组const fn vi.fn() const context {} fn.apply(context) fn.call(context) fn.mock.contexts[0] context fn.mock.contexts[1] context注意createMock内部对普通调用记录this、对new调用记录为undefinedconst context new.target ? undefined : this见 packages/spy/src/index.ts。mock.instancesconst instances: ReturnTypeT[]一个包含所有通过new关键字调用 Mock 时创建的实例的数组。注意这是函数的实际上下文this而不是返回值。::: warning 如果用new MyClass()实例化 Mock则mock.instances是一个包含一个值的数组const MyClass vi.fn() const a new MyClass() MyClass.mock.instances[0] a如果构造函数返回了值它不会出现在instances中而是出现在results里const Spy vi.fn(function () { return { method: vi.fn() } }) const a new Spy() Spy.mock.instances[0] ! a Spy.mock.results[0] a:::这一行为与源码中new.target分支的后续处理一致packages/spy/src/index.ts当通过Reflect.construct构造且构造函数返回了对象时state.instances与state.contexts中记录的值会被替换为实际返回的returnValue。八、在断言中运用 Mock 状态官方文档建议使用 expect 上的 Mock 断言来验证 Mock 行为。仓库中这些断言的注册位于 packages/expect/src/jest-expect.ts包括toHaveBeenCalledTimes/toBeCalledTimes断言调用次数toHaveBeenCalledOnce断言恰好调用一次toHaveBeenCalled/toBeCalled断言至少调用一次toHaveBeenCalledWith/toBeCalledWith断言以特定参数调用过toHaveBeenCalledExactlyOnceWith断言恰好以指定参数调用一次toHaveBeenCalledBefore/toHaveBeenCalledAfter断言调用先后顺序内部依赖invocationCallOrder语义。这些断言内部读取的正是上文介绍的mock.calls、mock.results、mock.invocationCallOrder等状态因此理解状态属性的语义是正确编写断言的前提。九、完整实战示例结合 test/unit/test/mocking/vi-fn.test.ts 中的真实用例风格以下是一个综合运用本文知识点的完整示例import { describe, expect, test, vi } from vitest describe(order service mocking, () { test(tracks calls, results and settled results, async () { const api { fetchOrders: async (userId: number) [order-${userId}], } // 替换实现并跟踪调用 const spy vi.spyOn(api, fetchOrders) .mockResolvedValueOnce([first]) .mockResolvedValue([default]) const p1 api.fetchOrders(1) const p2 api.fetchOrders(2) expect(spy.mock.calls).toEqual([[1], [2]]) expect(spy.mock.lastCall).toEqual([2]) expect(spy.mock.invocationCallOrder).toHaveLength(2) await Promise.all([p1, p2]) expect(spy.mock.settledResults.map(r r.type)).toEqual([fulfilled, fulfilled]) expect(spy.mock.settledResults[0].value).toEqual([first]) expect(spy.mock.settledResults[1].value).toEqual([default]) // 清理调用记录但保留实现 spy.mockClear() expect(spy.mock.calls).toEqual([]) // 完全恢复原实现 spy.mockRestore() await expect(api.fetchOrders(3)).resolves.toEqual([order-3]) }) })结语Vitest 的 Mock API 围绕记录 操控 恢复三个维度设计vi.fn/vi.spyOn负责创建与挂载mockImplementation*、mockReturn*、mockResolved*、mockRejected*、mockThrow*负责行为注入mockClear/mockReset/mockRestore负责生命周期管理而mock.calls、mock.results、mock.settledResults、mock.invocationCallOrder、mock.contexts、mock.instances等状态属性则为断言提供数据基础。通过阅读 packages/spy/src/index.ts 与 packages/spy/src/types.ts 的源码实现可以更深入地理解 Once 实现消费顺序、原型链重定向、Promise 落定记录等关键机制从而在复杂场景下写出精准可靠的测试。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表