多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Jest Mock 函数 API 完全指南:从 jest.fn() 到 mock 数据断言与 TypeScript 类型收窄

Jest Mock 函数 API 完全指南:从 jest.fn() 到 mock 数据断言与 TypeScript 类型收窄 Jest Mock 函数 API 完全指南从 jest.fn() 到 mock 数据断言与 TypeScript 类型收窄【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jestMock 函数Mock Functions是 Jest 测试框架的核心能力之一它允许你间谍化spy某个被其他代码间接调用的函数从而不仅验证输出结果还能观测它的调用次数、传参、返回值与this上下文。本文以 Jest 30.4 的官方 Mock 函数 API 文档为主体结合当前仓库中 jest-mock 包的源码实现系统讲解jest.fn()创建的每个方法与属性、jest.spyOn()/jest.replaceProperty()的替换与还原机制以及完整的 TypeScript 类型用法。读完本文你将能够写出可观测、可编排、可清理的高质量 Mock 函数并让它们在类型安全的前提下工作。什么是 Mock 函数SpyMock 函数之所以又被称为 spies间谍是因为它们让你能够观测一个被其他代码间接调用的函数的行为而不仅仅是测试其输出。你可以通过jest.fn()创建一个 mock 函数const mockFn jest.fn();如果不提供实现implementationmock 函数被调用时会返回undefined。在此基础上mock 函数会自动记录每一次调用的参数、结果、this上下文以及通过new实例化的对象这些记录数据可以从mockFn.mock中读取用于后续断言。mock 数据属性观测每一次调用每次调用 mock 函数时Jest 都会把调用信息记录到mockFn.mock对象中。从 jest-mock/src/index.ts 的_defaultMockState()可以看到mock 状态包含五个数组calls、contexts、instances、invocationCallOrder和results。mockFn.mock.calls一个数组包含对该 mock 函数所有调用的参数。数组中的每一项都是该次调用传入的参数数组。例如 mock 函数f被调用两次第一次传入f(arg1, arg2)第二次传入f(arg3, arg4)则mock.calls为[ [arg1, arg2], [arg3, arg4], ];mockFn.mock.results一个数组包含该 mock 函数所有调用的结果。每一项是一个对象包含type与value两个属性其中type取以下值之一return该调用正常返回throw该调用抛出了某个值incomplete该调用尚未完成。如果你在 mock 函数自身内部、或由 mock 调用的某个函数内部去检查结果就会出现此状态。value属性保存被返回或被抛出的值当type incomplete时value为undefined。例如 mock 函数f被调用三次依次返回result1、抛出错误、返回result2则mock.results为[ { type: return, value: result1, }, { type: throw, value: {/* Error instance */}, }, { type: return, value: result2, }, ];这一设计在源码 index.ts 中有明确体现mock 被调用时会立即以incomplete状态压入results避免递归场景下结果记录顺序错乱随后在finally块中根据是否抛错改写为throw或return见 index.ts。mockFn.mock.instances一个数组包含所有通过new从该 mock 函数实例化出来的对象。例如 mock 被实例化两次const mockFn jest.fn(); const a new mockFn(); const b new mockFn(); mockFn.mock.instances[0] a; // true mockFn.mock.instances[1] b; // true在源码中每当 mock 以构造函数形式被调用时this会被推入instances见 index.ts。mockFn.mock.contexts一个数组包含该 mock 函数所有调用的上下文context。上下文即函数被调用时接收到的this值可以通过Function.prototype.bind、Function.prototype.call或Function.prototype.apply来设置。例如const mockFn jest.fn(); const boundMockFn mockFn.bind(thisContext0); boundMockFn(a, b); mockFn.call(thisContext1, a, b); mockFn.apply(thisContext2, [a, b]); mockFn.mock.contexts[0] thisContext0; // true mockFn.mock.contexts[1] thisContext1; // true mockFn.mock.contexts[2] thisContext2; // true源码中调用发生时同样会执行mockState.contexts.push(this)见 index.ts。mockFn.mock.lastCall一个数组包含该 mock 函数最后一次调用的参数。如果函数从未被调用返回undefined。沿用前面的例子最后一次调用是f(arg3, arg4)则[arg3, arg4];从实现看lastCall并非单独存储的字段而是在读取 mock 状态时通过state.calls.at(-1)动态计算得到见 index.ts。查询与命名getMockImplementation 与 getMockNamemockFn.getMockImplementation()返回当前 mock 函数的实现即由mockImplementation()设置的那个函数如果未设置实现则返回undefined。const mockFn jest.fn(); mockFn.getMockImplementation(); // undefined mockFn.mockImplementation(() 42); mockFn.getMockImplementation(); // () 42TypeScript 版本注意以下 TypeScript 示例要求显式从jest/globals导入 Jest APIimport {jest} from jest/globals; const mockFn jest.fn() number(); mockFn.getMockImplementation(); // undefined mockFn.mockImplementation(() 42); mockFn.getMockImplementation(); // () 42mockFn.getMockName()返回通过调用.mockName()设置的 mock 名称字符串。从源码 index.ts 可以看到若未显式命名默认返回jest.fn()。清理、重置与还原mockClear / mockReset / mockRestoremockFn.mockClear()清空存储在mockFn.mock.calls、mockFn.mock.instances、mockFn.mock.contexts与mockFn.mock.results数组中的所有信息。常用于在两个断言之间清理某个 mock 的使用数据。mockFn.mockClear();:::caution 注意mockFn.mockClear()会整体替换mockFn.mock而不只是重置其内部属性的值因此应避免把mockFn.mock赋给其他变量无论临时与否以免读到过期数据。源码中mockClear的实现就是直接从状态注册表中删除该 mock 的状态见 index.ts。 :::另外配置项clearMocksclearMocks: boolean可以在每个测试之前自动清空 mock无需手动调用。mockFn.mockReset()完成mockFn.mockClear()所做的一切并且把 mock 实现替换为一个返回undefined的空函数。配置项resetMocksresetMocks: boolean可以在每个测试之前自动重置 mock。mockFn.mockRestore()完成mockFn.mockReset()所做的一切并且恢复原始的非 mock 的实现。当你想在某些测试用例中 mock 函数、而在其他用例中恢复原始实现时非常有用。mockFn.mockRestore();:::infomockFn.mockRestore()仅当 mock 是由jest.spyOn()创建时才生效。如果手动使用jest.fn()赋值你需要自己负责还原工作。 :::配置项restoreMocksrestoreMocks: boolean可以在每个测试之前自动还原 mock。从源码 index.ts 可以看到三者的调用关系mockReset先调用mockClear再删除 mock 配置mockRestore先调用mockReset再执行由spyOn/replaceProperty注入的restore还原闭包。设置实现mockImplementation 与 mockImplementationOncemockFn.mockImplementation(fn)接收一个函数作为 mock 的实现。mock 本身仍会记录所有进入的调用与产生的实例——唯一区别是mock 被调用时会执行这个实现。:::tipjest.fn(implementation)是jest.fn().mockImplementation(implementation)的简写。 :::const mockFn jest.fn(scalar 42 scalar); mockFn(0); // 42 mockFn(1); // 43 mockFn.mockImplementation(scalar 36 scalar); mockFn(2); // 38 mockFn(3); // 39TypeScript 版本import {jest} from jest/globals; const mockFn jest.fn((scalar: number) 42 scalar); mockFn(0); // 42 mockFn(1); // 43 mockFn.mockImplementation(scalar 36 scalar); mockFn(2); // 38 mockFn(3); // 39.mockImplementation()也可以用来 mock 类的构造函数。假设存在一个类module.exports class SomeClass { method(a, b) {} };在测试中我们可以 mock 整个模块并让构造函数返回一个包含 mock 方法的对象const SomeClass require(./SomeClass); jest.mock(./SomeClass); // this happens automatically with automocking const mockMethod jest.fn(); SomeClass.mockImplementation(() { return { method: mockMethod, }; }); const some new SomeClass(); some.method(a, b); console.log(Calls to method:, mockMethod.mock.calls);TypeScript 版本export class SomeClass { method(a: string, b: string): void {} }import {jest} from jest/globals; import {SomeClass} from ./SomeClass; jest.mock(./SomeClass); // this happens automatically with automocking const mockMethod jest.fn(a: string, b: string) void(); jest.mocked(SomeClass).mockImplementation(() { return { method: mockMethod, }; }); const some new SomeClass(); some.method(a, b); console.log(Calls to method:, mockMethod.mock.calls);mockFn.mockImplementationOnce(fn)接收一个函数作为 mock 函数某一次调用的实现。它可以被链式调用使多次函数调用产生不同结果const mockFn jest .fn() .mockImplementationOnce(cb cb(null, true)) .mockImplementationOnce(cb cb(null, false)); mockFn((err, val) console.log(val)); // true mockFn((err, val) console.log(val)); // falseTypeScript 版本import {jest} from jest/globals; const mockFn jest .fn(cb: (a: null, b: boolean) void) void() .mockImplementationOnce(cb cb(null, true)) .mockImplementationOnce(cb cb(null, false)); mockFn((err, val) console.log(val)); // true mockFn((err, val) console.log(val)); // false当.mockImplementationOnce()定义的实现被用完后mock 会回退到默认实现——即jest.fn(() defaultValue)或.mockImplementation(() defaultValue)设置的那个函数const mockFn jest .fn(() default) .mockImplementationOnce(() first call) .mockImplementationOnce(() second call); mockFn(); // first call mockFn(); // second call mockFn(); // default mockFn(); // default这一先用一次性实现、再用默认实现的调度逻辑在源码中非常清晰mock 被调用时先specificMockImpls.shift()取出一次性实现取不到才回退到mockImpl再回退到原型实现最后返回undefined见 index.ts。mockFn.withImplementation(fn, callback)接收一个函数在callback 执行期间临时作为 mock 的实现test(test, () { const mock jest.fn(() outside callback); mock.withImplementation( () inside callback, () { mock(); // inside callback }, ); mock(); // outside callback });mockFn.withImplementation不限制 callback 是否异步是否返回 thenable。如果 callback 是异步的它会返回一个 Promiseawait该 Promise 会等待 callback 执行完毕并重置实现test(async test, async () { const mock jest.fn(() outside callback); // We await this call since the callback is async await mock.withImplementation( () inside callback, async () { mock(); // inside callback }, ); mock(); // outside callback });源码 index.ts 展示了其实现原理先保存当前的mockImpl、specificMockImpls与fallbackImpl换成临时实现并清空一次性实现队列执行 callback若返回 Promise则在 resolve 后恢复原实现否则同步恢复。便捷返回值方法以下方法都是mockImplementation/mockImplementationOnce的语法糖专门用于快速设定返回值。在源码中这些方法被明确实现为对mockImplementation与mockImplementationOnce的调用见 index.ts。mockFn.mockReturnThis()是下面这段代码的简写——mock 被调用时返回this常用于链式调用场景jest.fn(function () { return this; });mockFn.mockReturnValue(value)是jest.fn().mockImplementation(() value)的简写接收一个每次调用都会返回的值const mock jest.fn(); mock.mockReturnValue(42); mock(); // 42 mock.mockReturnValue(43); mock(); // 43TypeScript 版本import {jest} from jest/globals; const mock jest.fn() number(); mock.mockReturnValue(42); mock(); // 42 mock.mockReturnValue(43); mock(); // 43mockFn.mockReturnValueOnce(value)是jest.fn().mockImplementationOnce(() value)的简写接收一个只对某一次调用生效的返回值。可以链式调用让连续多次调用返回不同值当所有mockReturnValueOnce用完时后续调用回退到mockReturnValue指定的值const mockFn jest .fn() .mockReturnValue(default) .mockReturnValueOnce(first call) .mockReturnValueOnce(second call); mockFn(); // first call mockFn(); // second call mockFn(); // default mockFn(); // defaultTypeScript 版本import {jest} from jest/globals; const mockFn jest .fn() string() .mockReturnValue(default) .mockReturnValueOnce(first call) .mockReturnValueOnce(second call); mockFn(); // first call mockFn(); // second call mockFn(); // default mockFn(); // defaultmockFn.mockResolvedValue(value)是jest.fn().mockImplementation(() Promise.resolve(value))的简写适合在异步测试中 mock 异步函数test(async test, async () { const asyncMock jest.fn().mockResolvedValue(43); await asyncMock(); // 43 });TypeScript 版本import {jest, test} from jest/globals; test(async test, async () { const asyncMock jest.fn() Promisenumber().mockResolvedValue(43); await asyncMock(); // 43 });mockFn.mockResolvedValueOnce(value)是jest.fn().mockImplementationOnce(() Promise.resolve(value))的简写用于让多次异步调用依次 resolve 不同值test(async test, async () { const asyncMock jest .fn() .mockResolvedValue(default) .mockResolvedValueOnce(first call) .mockResolvedValueOnce(second call); await asyncMock(); // first call await asyncMock(); // second call await asyncMock(); // default await asyncMock(); // default });TypeScript 版本import {jest, test} from jest/globals; test(async test, async () { const asyncMock jest .fn() Promisestring() .mockResolvedValue(default) .mockResolvedValueOnce(first call) .mockResolvedValueOnce(second call); await asyncMock(); // first call await asyncMock(); // second call await asyncMock(); // default await asyncMock(); // default });mockFn.mockRejectedValue(value)是jest.fn().mockImplementation(() Promise.reject(value))的简写用于创建总是 reject的异步 mock 函数test(async test, async () { const asyncMock jest .fn() .mockRejectedValue(new Error(Async error message)); await asyncMock(); // throws Async error message });TypeScript 版本import {jest, test} from jest/globals; test(async test, async () { const asyncMock jest .fn() Promisenever() .mockRejectedValue(new Error(Async error message)); await asyncMock(); // throws Async error message });mockFn.mockRejectedValueOnce(value)是jest.fn().mockImplementationOnce(() Promise.reject(value))的简写常与.mockResolvedValueOnce()配合使用或在多次异步调用中依次 reject 不同的异常test(async test, async () { const asyncMock jest .fn() .mockResolvedValueOnce(first call) .mockRejectedValueOnce(new Error(Async error message)); await asyncMock(); // first call await asyncMock(); // throws Async error message });TypeScript 版本import {jest, test} from jest/globals; test(async test, async () { const asyncMock jest .fn() Promisestring() .mockResolvedValueOnce(first call) .mockRejectedValueOnce(new Error(Async error message)); await asyncMock(); // first call await asyncMock(); // throws Async error message });给 mock 命名mockNamemockFn.mockName(name)接收一个字符串在测试结果输出中替代jest.fn()用于指明正在引用的是哪一个 mock 函数。例如const mockFn jest.fn().mockName(mockedFunction); // mockFn(); expect(mockFn).toHaveBeenCalled();会得到如下错误信息expect(mockedFunction).toHaveBeenCalled() Expected number of calls: 1 Received number of calls: 0注意错误信息中显示的是mockedFunction而不是jest.fn()这正是mockName的价值所在——当测试文件中存在大量 mock 时可以快速定位是哪个 mock 断言失败。源码实现也非常直接mockName把名称写入 mock 配置getMockName读取时若无名称则回退到jest.fn()见 index.ts。替换属性jest.replaceProperty 与 Replaced Properties除了函数Jest 30 还支持直接替换对象的属性值。jest.replaceProperty(object, propertyKey, value)会返回一个Replaced对象用于后续调整或还原详见 JestObjectAPI。replacedProperty.replaceValue(value)改变已替换属性的值。当你先替换属性、再在特定测试中调整其值时非常有用另一种做法是多次对同一属性调用jest.replaceProperty()。从源码看如果同一属性已存在替换记录新的replaceProperty调用会复用原记录并直接调用replaceValue见 index.ts。replacedProperty.restore()把对象的属性恢复为原始值。:::inforeplacedProperty.restore()仅当属性是通过jest.replaceProperty()替换时才生效。 :::配置项restoreMocks同样会在每个测试之前自动还原被替换的属性。从源码实现看replaceProperty有一系列严格的约束见 index.ts目标必须是对象或函数不能是原始值属性必须存在且可配置configurable如果属性有 getter会提示改用jest.spyOn(object, key, get).mockReturnValue(value)如果属性有 setter会提示改用set如果属性值是函数会提示改用jest.spyOn。还原时若属性是自身属性则写回原值否则删除属性以重新暴露原型链上的值见 index.ts。TypeScript 用法让 mock 拥有正确类型:::info 本页所有 TypeScript 示例只有在你显式导入 Jest API 时才能按文档所述正常工作import {expect, jest, test} from jest/globals;如何配置 Jest 使用 TypeScript请参阅 Getting Started 指南。 :::jest.fn(implementation?)当把实现传给jest.fn()时Jest 能推断出正确的 mock 类型。但在很多场景下实现会被省略此时为了保证类型安全可以传入泛型类型参数更多参考见上文各 TypeScript 示例import {expect, jest, test} from jest/globals; import type add from ./add; import calculate from ./calc; test(calculate calls add, () { // Create a new mock that can be used in place of add. const mockAdd jest.fntypeof add(); // .mockImplementation() now can infer that a and b are number // and that the returned value is a number. mockAdd.mockImplementation((a, b) { // Yes, this mock is still adding two numbers but imagine this // was a complex function we are mocking. return a b; }); // mockAdd is properly typed and therefore accepted by anything // requiring add. calculate(mockAdd, 1, 2); expect(mockAdd).toHaveBeenCalledTimes(1); expect(mockAdd).toHaveBeenCalledWith(1, 2); });jest.MockT构造一个 mock 函数的类型例如jest.fn()的返回类型。当你需要定义递归的 mock 函数时非常有用import {jest} from jest/globals; const sumRecursively: jest.Mock(value: number) number jest.fn(value { if (value 0) { return 0; } else { return value fn(value - 1); } });jest.MockedSourcejest.MockedSource工具类型会把Source类型包装上 Jest mock 函数的类型定义import {expect, jest, test} from jest/globals; import type {fetch} from node-fetch; jest.mock(node-fetch); let mockedFetch: jest.Mockedtypeof fetch; afterEach(() { mockedFetch.mockClear(); }); test(makes correct call, () { mockedFetch getMockedFetch(); // ... }); test(returns correct data, () { mockedFetch getMockedFetch(); // ... });类、函数或对象类型都可以作为jest.MockedSource的类型参数。如果你想约束输入类型可以使用jest.MockedClassSource、jest.MockedFunctionSource或jest.MockedObjectSource。jest.ReplacedSourcejest.ReplacedSource工具类型返回被 Jest replaced property 类型定义包装后的Source类型。以下示例 mock 了process.env来测试isLocalhost函数export function isLocalhost(): boolean { return process.env[HOSTNAME] localhost; }import {afterEach, expect, it, jest} from jest/globals; import {isLocalhost} from ../utils; let replacedEnv: jest.Replacedtypeof process.env | undefined undefined; afterEach(() { replacedEnv?.restore(); }); it(isLocalhost should detect localhost environment, () { replacedEnv jest.replaceProperty(process, env, {HOSTNAME: localhost}); expect(isLocalhost()).toBe(true); }); it(isLocalhost should detect non-localhost environment, () { replacedEnv jest.replaceProperty(process, env, {HOSTNAME: example.com}); expect(isLocalhost()).toBe(false); });jest.mocked(source, options?)mocked()辅助方法会把source对象及其深层嵌套成员的类型包装上 Jest mock 函数的类型定义。可以传入{shallow: true}作为options参数来禁用深层 mock 行为。返回值就是source对象本身。export const song { one: { more: { time: (t: number) { return t; }, }, }, };import {expect, jest, test} from jest/globals; import {song} from ./song; jest.mock(./song); jest.spyOn(console, log); const mockedSong jest.mocked(song); // or through jest.MockedSource // const mockedSong song as jest.Mockedtypeof song; test(deep method is typed correctly, () { mockedSong.one.more.time.mockReturnValue(12); expect(mockedSong.one.more.time(10)).toBe(12); expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); }); test(direct usage, () { jest.mocked(console.log).mockImplementation(() { return; }); console.log(one more time); expect(jest.mocked(console.log).mock.calls).toHaveLength(1); });jest.SpiedSource构造被 spy 的类或函数的类型即jest.spyOn()的返回类型。以下示例用jest.Spiedtypeof Date.now标注了setDateNow的返回类型import {jest} from jest/globals; export function setDateNow(now: number): jest.Spiedtypeof Date.now { return jest.spyOn(Date, now).mockReturnValue(now); }import {afterEach, expect, type jest, test} from jest/globals; import {setDateNow} from ./__utils__/setDateNow; let spiedDateNow: jest.Spiedtypeof Date.now | undefined undefined; afterEach(() { spiedDateNow?.mockReset(); }); test(renders correctly with a given date, () { spiedDateNow setDateNow(1_482_363_367_071); // ... expect(spiedDateNow).toHaveBeenCalledTimes(1); });类或函数类型都可以作为jest.SpiedSource的类型参数。如果你想约束输入类型可以使用jest.SpiedClassSource或jest.SpiedFunctionSource。若要构造被 spy 的 getter 或 setter 的类型则分别使用jest.SpiedGetterSource或jest.SpiedSetterSource。源码视角mock 的调度与记录原理理解以上 API 后再看 jest-mock/src/index.ts 中_makeComponent的实现可以更清晰地把握整体机制调用记录每次调用都会按序执行instances.push(this)、contexts.push(this)、calls.push(args)并预置一条incomplete的 result 记录同时通过全局计数器维护invocationCallOrder见 index.ts。实现调度调用时优先消费specificMockImplsmockImplementationOnce系列压入的队列取不到再使用mockImplmockImplementation/ 各种mockReturnValue/mockResolvedValue设置的默认实现再回退到_protoImpl原型实现最终返回undefined见 index.ts。jest.fn(impl)与jest.fn()创建完成后如果元数据中带有mockImpl会自动调用mockImplementation完成初始化见 index.ts。spyOn与还原spyOn会保存原始函数并为 mock 安装一个restore闭包见 index.tsgetter/setter 场景则由_spyOnProperty通过替换属性描述符实现见 index.ts这正是mockRestore()只在spyOn创建的 mock 上生效的原因。此外仓库中 packages/jest-mock/typetests目录下的Mocked.test.ts、utility-types.test.ts、mock-functions.test.ts等类型测试文件以及 packages/jest-mock/src/tests/index.test.ts 中针对mockImplementation、mockReturnValue、spyOn、replaceProperty等行为的单元测试为本文所述的各 API 行为提供了直接的可运行验证依据读者可以深入这些文件进一步研究边界行为例如递归调用、mockClear期间被调用等场景。小结Mock 函数 API 是 Jest 测试生态的基石jest.fn()提供可观测、可编排的函数替身mock.calls/mock.results/mock.instances/mock.contexts/mock.lastCall让每次调用的细节尽在掌握mockClear/mockReset/mockRestore配合clearMocks/resetMocks/restoreMocks配置项让测试间的状态隔离成为惯例mockImplementation(Once)与mockReturnValue(Once)/mockResolvedValue(Once)/mockRejectedValue(Once)系列则覆盖了同步与异步场景下几乎所有返回值编排需求。配合jest.replaceProperty()的属性替换能力以及jest.MockT、jest.MockedSource、jest.mocked()、jest.SpiedSource、jest.ReplacedSource等类型工具你可以写出既行为精确又类型安全的测试代码。本文对应的原始 API 文档位于 website/versioned_docs/version-30.4/MockFunctionAPI.md实现代码位于 packages/jest-mock/src/index.ts可供继续查阅。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表