ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Egg 单元测试 Mock 模式实战指南:@eggjs/mock 的 mm()、mockHttpclient 与 mockCsrf 全解析

2026/9/21 1:43:23 拓冰建站 浏览量
Egg 单元测试 Mock 模式实战指南:@eggjs/mock 的 mm()、mockHttpclient 与 mockCsrf 全解析 后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载导读本文是 EGG 单元测试技能egg-unittest中 Mock 模式references/mock.md的完整实战指南聚焦eggjs/mock提供的四种核心 Mock 手段mm()原型方法 Mock、mm.spy()调用记录、app.mockHttpclient()外部 HTTP 请求 Mock、app.mockCsrf()跳过 CSRF 校验。读完本文你将掌握在 Vitest eggjs/mock/bootstrap测试环境下对 DI依赖注入对象、外部 API、安全校验进行精确打桩与断言的方法并理解其底层实现原理与自动恢复机制。一、为什么需要 Mock 模式在 EGG 应用中单元测试的目标是隔离被测对象、控制外部依赖。常见需要 Mock 的场景包括被测 Service 依赖其他 DI 对象如OrderService依赖UserService、NotifyService业务代码通过Inject() httpclient: HttpClient发起的外部 HTTP 调用第三方 API、支付网关等POST/PUT/DELETE 请求触发安全插件的 CSRF 校验导致 403。Mock 的核心价值是在不修改业务代码的前提下替换或观察依赖行为从而让测试稳定、快速、可重复。测试运行环境由 egg-binVitest启动app实例与mm工具从eggjs/mock/bootstrap统一导入。二、常见错误速查表错误写法正确写法说明mm(service, method, fn)mm(ServiceClass.prototype, method, fn)DI 对象需 mock 原型不是实例手动写afterEach(mm.restore)不需要egg-bin 自动注入 mock 恢复new Ajv()mock 单独实例mock 原型方法DI 容器管理的对象通过原型 mock为什么必须是原型而非实例EGG 的 tegg DI 容器在运行时通过app.getEggObject(Class)获取对象实例实例的方法来自其原型链。如果对某个具体实例service直接mm(service, method, fn)该替换只存在于这一个实例上而测试代码通过app.getEggObject()拿到的往往是容器新建或复用的另一个实例Mock 根本不会生效。因此必须mm(ServiceClass.prototype, method, fn)从类型定义层面全局生效。这一规则与mm()的源码实现基于mm库对目标对象属性进行替换一致见 plugins/mock/src/index.ts。三、mm() — Mock Proto 方法mm()是最常用的 Mock 方式用于替换 DI 对象的原型方法使其返回固定数据或执行自定义逻辑。import assert from node:assert; import { app, mm } from eggjs/mock/bootstrap; import { UserService } from ../app/modules/user/UserService.ts; import { OrderService } from ../app/modules/order/OrderService.ts; describe(OrderService, () { it(should mock user service, async () { mm(UserService.prototype, getById, async () { return { id: 1, name: mocked user }; }); const orderService await app.getEggObject(OrderService); const result await orderService.createForUser(1); assert.equal(result.userName, mocked user); }); });要点Mock 函数应为async 函数若原始方法是异步方法保证 Promise 语义一致通过await app.getEggObject(OrderService)获取被测 DI 对象断言应针对业务结果result.userName mocked user而非 Mock 函数本身验证OrderService正确消费了UserService的返回值。调用信息断言called / calledArguments / lastCalledArgumentsMock 函数会自动记录调用信息可用来断言是否正确调用、以什么参数调用import assert from node:assert; import { app, mm } from eggjs/mock/bootstrap; import { NotifyService } from ../app/modules/notify/NotifyService.ts; import { OrderService } from ../app/modules/order/OrderService.ts; it(should call notify with correct args, async () { const mockFn async (userId: string, message: string) {}; mm(NotifyService.prototype, send, mockFn); const orderService await app.getEggObject(OrderService); await orderService.create({ productId: 1 }); assert.equal(mockFn.called, 1); // 调用次数 assert.deepStrictEqual(mockFn.lastCalledArguments, [user-1, 订单创建成功]); // 最后一次调用参数 // mockFn.calledArguments — 所有调用参数的数组 });Mock 函数上自动附加的断言属性属性含义示例用法called被调用次数assert.equal(mockFn.called, 1)calledArguments所有调用参数的数组二维assert.deepStrictEqual(mockFn.calledArguments, [[user-1, msg]])lastCalledArguments最后一次调用的参数列表assert.deepStrictEqual(mockFn.lastCalledArguments, [user-1, msg])这种行为打桩 调用记录的组合是验证 Service 之间协作关系的标准手法。四、mm.spy() — 不替换实现只记录调用当希望原方法正常执行、同时又想观察它是否被调用时使用mm.spy()it(should spy on method, async () { mm.spy(NotifyService.prototype, send); const orderService await app.getEggObject(OrderService); await orderService.create({ productId: 1 }); // 原方法正常执行同时记录了调用信息 const sendFn NotifyService.prototype.send; assert.equal(sendFn.called, 1); assert.equal(sendFn.lastCalledArguments[0], user-1); });与mm()的区别mm()用自定义函数替换原实现控制返回值/行为mm.spy()保留原实现仅附加called、calledArguments、lastCalledArguments等记录属性用于确认发生过调用、参数正确的验证型断言由于原型方法被记录属性包装断言可直接从NotifyService.prototype.send读取。适用场景日志发送、消息通知、埋点上报等副作用型方法——你关心的是有没有调、参数对不对而不是返回值。五、app.mockHttpclient() — Mock HttpClient 请求业务代码通过Inject() httpclient: HttpClient注入的 HttpClient 发起的请求可用app.mockHttpclient()整体打桩避免测试真正访问外部网络it(should mock external API, () { app.mockHttpclient(https://api.example.com/users, { data: JSON.stringify({ name: test }), }); return app.httpRequest().get(/api/proxy/users).expect(200).expect({ name: test }); });参数签名与重载实现见 plugins/mock/src/app/extend/application.ts 与 plugins/mock/src/lib/mock_httpclient.tsmockHttpclient( mockUrl: string | RegExp, // 匹配的 URL支持正则 mockMethod?: string | string[], // HTTP 方法默认 * mockResult?: string | MockResultOptions | MockResultFunction, ): this二参重载app.mockHttpclient(url, mockResult)时mockMethod默认为*匹配所有方法三参重载app.mockHttpclient(url, GET, mockResult)指定方法方法会统一转为大写mockUrl为字符串时解析为origin pathname匹配为RegExp时按origin path整体正则匹配可命中多条 Mock 配置。MockResultOptions 完整字段字段类型默认值说明datastring \| Buffer \| Object响应体Object 会被自动JSON.stringify序列化statusnumber200HTTP 状态码headersRecordstring, string{}响应头delaynumber无延迟响应的毫秒数可模拟慢接口persistbooleantrue是否无限次命中该 Mock默认始终生效repeatsnumber无固定命中次数persist: false时生效源码层面的行为细节mock_httpclient.tsdata为对象时转为 JSON Buffer为字符串时按 UTF-8 编码为 Buffer否则抛错底层基于urllib的 MockAgentundici 拦截器实现persist默认true意味着同一条 URL 的 Mock 在恢复前会一直命中同一测试内多次调用无需重复声明mockResult也支持函数形式(url, options) MockResultOptions | string可依据请求path、method、body动态返回结果适合区分同一接口的不同请求场景。组合使用建议外部 API 的 Mock 常与接口测试链式配合先用mockHttpclient挡住下游 HTTP 依赖再通过app.httpRequest()supertest驱动 Controller 完整链路最终只断言业务侧响应。六、app.mockCsrf() — 跳过 CSRF 校验POST/PUT/DELETE 请求默认会触发安全插件的 CSRF 校验未携带 token 时返回 403。测试中可直接跳过it(should POST without CSRF error, () { app.mockCsrf(); return app.httpRequest().post(/api/users).send({ name: test }).expect(200); });实现位于 application.tsmockCsrf(): this { mock(this.context, assertCSRF, () {}); mock(this.context, assertCsrf, () {}); return this; }即对 Context 原型上的assertCSRF/assertCsrf方法做空替换使校验直接通过。使用注意只需在需要写操作的测试中调用GET 等读操作无需该方法只影响当前app实例afterEach恢复后自动失效若测试本身要验证 CSRF 防护逻辑如 403 场景则不要调用mockCsrf()。七、Mock 恢复自动注入无需手写egg-bin 自动注入eggjs/mock/setup_vitest该模块在 Vitest 生命周期中自动完成三件事beforeAll单次启动 MockApplicationapp并对启动 Promise 做缓存同一 worker 内只启动一次afterEach等待后台任务结束app.backgroundTasksFinished()并调用mm.restore()恢复所有 MockafterAll按共享模式判断是否关闭 appisolate/threads 池场景下由 worker 线程回收。因此不要手动编写afterEach(mm.restore)重复恢复没有意义且可能干扰生命周期restore()的实现restore.ts会依次执行mm库恢复、cluster 恢复、MockAgent 恢复保证mm()、mm.spy()、mockHttpclient、mockCsrf等所有 Mock 全部还原到初始状态测试之间互不污染。八、与其他 Mock API 的配合在 DI 场景下mm()通常与以下 API 组合使用构成完整测试体系详见 egg-unittest 技能总览API用途app.getEggObject(Class)获取 SingletonProto / ContextProto 实例app.mockContext(data)Mock Context 属性app.mockService(name, method, fn)Mock Service字符串路径或类app.mockServiceError(name, method, err)Mock Service 抛出指定错误app.mockSession(data)/app.mockCookies(obj)Mock Session / Cookieapp.mockHeaders(headers)/app.mockLog()Mock 请求头 / 捕获日志mm.env(env)/app.mockEnv(env)Mock 运行环境test/prod 等需要 Mock 单测依赖而当前测试文件又没有app生命周期时可参考mm.app()/mm.cluster()的创建入口index.ts手动创建 MockApplication。九、总结EGG 单元测试的 Mock 模式可以归纳为三条原则DI 对象一律 Mock 原型mm(Class.prototype, method, fn)不要 Mock 实例验证型场景用mm.spy()控制型场景用mm()前者记录调用、保留实现后者替换实现、决定返回外部依赖与安全校验交给 app 级 APIapp.mockHttpclient()拦截 HttpClientapp.mockCsrf()跳过 CSRF恢复由setup_vitest的afterEach自动完成。按此模式编写测试即可获得稳定、隔离、可断言的 Service/DI 单元测试为 EGG 应用的持续集成提供可靠保障。赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载相关推荐手把手教你搭建i茅台智能预约系统从零开始实现自动化预约手把手教你搭建i茅台智能预约系统从零开始实现自动化预约 还在为每天9点准时抢购茅台而烦恼吗还在因为手速不够快而错过预约机会吗今天我要为你介绍一个 i茅台智后端Web框架掌握Egg.js单元测试从Mock到Stub的实战进阶指南掌握Egg.js单元测试从Mock到Stub的实战进阶指南 Egg.js作为基于Node.js和Koa的企业级框架其强大的测试体系是保障应用质量的关键。本文后端Web框架如何在10分钟内完成AI语音克隆训练Retrieval-based-Voice-Conversion-WebUI终极指南如何在10分钟内完成AI语音克隆训练Retrieval based Voice Conversion WebUI终极指南 你是否曾梦想过拥有自己的专属AI语音人工智能AI 应用语音音频深度学习上一篇打造无屏编程体验claude-code-local语音交互模式全攻略下一篇VirtualApp CI/CD监控监控构建和部署状态创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考