ARTICLE DETAIL

资讯详情

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

2024 Mock实战复盘:从工具选型到接口测试落地与异常注入

2024 Mock实战复盘:从工具选型到接口测试落地与异常注入 做联调的时候最怕什么等。前端等后端接口测试等前端提测后端等第三方服务响应。一圈等下来一天就没了。Mock这个老话题2024年反而成了进阶的刚需——不是会不会用的问题而是能不能把Mock用到体系化、用到能解决真实协作问题。这篇文章不是来科普Mock定义的是把我这一整年踩过坑、填过土、最后沉淀下来的Mock实战经验做一次完整复盘。内容包括主流工具选型对比、接口测试场景下的落地步骤、Easy Mock这类公共服务挂掉时的替代方案以及动态Mock、异常注入、契约测试这类进阶玩法。适合前端、测试、全栈开发者尤其是团队里正在被联调效率折磨的人。1. 先聊清楚Mock解决的到底是什么问题1.1 联调卡点与依赖倒置研发进度的隐形杀手很多人觉得Mock就是“造假数据”这话对了一半。Mock的本质是用一个可控的替身把不确定的依赖从你的开发链路里摘出去。想想实际开发里的典型场景后端接口还没写完前端页面已经排期了。按正常流程前端只能等等到后端把接口交付前端才能开始调。这一等可能就是三到五天。测试也一样测试环境没有真实数据或者第三方支付、地图、短信这类外部服务没法在测试环境真实调通用例就只能干躺着。2024年的研发节奏早就变了前后端并行、微服务拆分、外部依赖云服务化一条链路上可能挂了好几个你控制不了的服务。如果每个依赖都等到“真实联调”才能验证那项目节奏基本废了。Mock的价值就在这里它让前端可以按接口文档提前开发让测试可以在没有真实服务的情况下跑用例让演示Demo不依赖任何外部环境。一句话Mock把并行开发从理想变成了现实。1.2 三类典型使用场景对号入座我复盘了一下自己用Mock的场景基本可以归成三类覆盖了绝大多数需求场景类型要解决的问题常用方案注意点前端并行开发后端接口未完成页面交互需要数据驱动Mock.js、MSW、自建Mock服务Mock数据必须贴近真实接口结构接口测试构造正常/异常返回值验证系统容错Charles改包、测试框架Mock、自建Mock服务异常场景模拟比正常场景更关键Demo演示现场演示不能依赖外网或真实数据JSON Server、静态Mock数据数据要稳定可复现不能每次演示效果不同这三类场景听起来简单但实际执行起来各有各的坑。前端并行开发最怕Mock数据和真实接口结构不一致等到联调那天全部返工接口测试最怕只测了正常流程异常分支全没覆盖Demo演示最怕网络环境一变现场翻车。后面的内容基本就是围绕这三类场景展开的实战内容和避坑记录。2. 2024年主流Mock工具怎么选横向对比与真实感受2.1 轻量级本地工具Charles/Fiddler的Mock边界先说说Charles热搜词里就有“charles mock数据教程”说明这套玩法确实有大量的人在查。Charles这类抓包工具的Mock能力核心就四个Map Local、Map Remote、Rewrite、Breakpoint。Map Local是本地文件映射把某个接口的响应替换成本地保存的JSON文件。操作用一条链路就能说清抓包拿到真实响应保存成JSON文件然后在Charles里右键选择Map Local指定本地文件路径再把这个接口的请求拦截到本地文件上。重启请求后返回的就是你改过的数据。Map Remote则是把请求重定向到另一个服务地址适合接口迁移或临时切环境。Rewrite用来做响应体或请求头的正则替换适合改一些结构化字段。Breakpoint是断点调试可以在请求发出前或响应返回前拦截手动编辑数据包内容适合精细控制单次请求。这套方案的优势是快、零代码、不需要改工程。但边界也很明显它更适合调试和单点验证不适合做批量Mock或动态逻辑。比如要根据不同查询参数返回不同数据用Charles就很难受只能手动切文件或反复改写。而且Charles的Mock依赖本地配置换一台机器就得重新配没法团队共享。2.2 代码级MockMock.js与MSW的实际体验代码级Mock是我个人用得最多的一类。Mock.js在热搜词里也有出现它做得最好的事情就是“根据模板生成随机数据”拦截XMLHttpRequest请求返回符合模板规则的数据。Mock.js核心玩的是数据模板语法。举个例子import Mock from mockjs; Mock.mock(/api/user/list, get, { code: 0, data|1-10: [{ id|1: 1, name: cname, email: email, status|1: [active, disabled, pending], createTime: datetime }] });data|1-10表示生成1到10条数据id|1表示自增cname是随机中文名email是随机邮箱。这套语法覆盖了数字区间、枚举值、字符串拼接、日期随机等常用需求写起来效率确实高。Mock.js的坑在于它拦截的是XHR请求如果项目用了fetch发请求Mock.js默认不生效需要额外适配。另外如果在console里看到网络请求是正常返回而不是pending状态说明拦截没生效要检查请求方式。Mock Service WorkerMSW是这两年比较新的方案它利用Service Worker在网络层做拦截不管是XHR还是fetch都能拦到。而且因为基于标准Service Worker API跟真实环境的兼容性更接近。MSW的使用逻辑是先在Service Worker里注册handler然后在测试或开发环境启动import { http, HttpResponse } from msw; import { setupWorker } from msw/browser; const handlers [ http.get(/api/user/:id, ({ params }) { return HttpResponse.json({ id: params.id, name: 测试用户, role: admin }); }) ]; const worker setupWorker(...handlers); worker.start();MSW的体验比Mock.js更贴近真实网络环境而且可以做条件分支按请求参数返回不同数据。缺点是方案相对复杂团队里其他成员上手成本高一些。2.3 平台化MockEasy Mock、YApi、Apifox这类工具怎么定位平台化Mock解决的是团队协作问题。Easy Mock之所以火是因为它把接口文档和Mock数据生成绑在了一起——后端在平台上定义好接口结构前端直接就能拿到Mock数据大家用的是同一份定义。Easy Mock的操作流程大致是创建项目在项目中创建接口配置URL、请求方法、Mock规则支持Mock.js语法然后前端通过项目专属URL访问Mock数据。但这里就要说到热搜词里那个扎心的问题“easy mock上不去”。我在2024年至少遇到三次Easy Mock访问不了的情况。官方免费服务长期处于不稳定状态有段时间域名解析都有问题依赖它的团队一旦遇上就集体抓瞎。这个我在第4章专门展开讲替代方案。YApi和Apifox这类工具则把Mock做成了附属能力YApi是接口管理平台能在定义接口时一键生成Mock数据Apifox则更进一步接口管理、调试、Mock、测试一体化Mock数据可以在接口定义里直接配置还支持根据字段类型自动生成随机数据。平台化工具的价值是数据统一、接口结构一致、团队共享但它们的通病是依赖服务端稳定性而且数据生成逻辑相对固定遇到复杂动态Mock需求往往力不从心。2.4 自建Mock服务什么时候值得这么做用了一段平台化工具之后我开始思考一个问题什么时候应该放弃现成工具自己搭Mock服务我的判断标准有三条第一Mock数据需要跑在持续集成环境里不能被外网服务的不稳定拖累第二Mock数据状态需要流转比如订单状态从“待支付”到“已支付”再到“已完成”需要可编程控制第三团队对Mock数据的版本有要求希望数据跟着代码仓库走而不是散落在某个平台。如果命中一到三条自建Mock服务就是划算的。技术栈上最轻量的是Node.js加Express配合JSON Server这类工具几十行代码就能跑起来。这个在后面实战章节我会给出可以直接复用的代码。3. 实战接口测试中Mock的落地方案与操作细节3.1 用Charles给第三方接口做Mock的完整流程接口测试里面最常用到Charles的场景是模拟第三方服务的各种异常返回。我之前做过一个对接微信支付回调的项目测试环境不可能真的调微信支付所以需要把回调接口的响应Mock掉。操作链路是这样的第一步配置SSL Proxying。Charles要抓HTTPS包得先在手机或模拟器上安装Charles的根证书然后在Charles的SSL Proxying Settings里添加需要抓包的域名。这里有个很容易踩的坑只装证书不配置SSL Proxying列表照样看不到HTTPS报文。第二步抓一次真实请求。在测试环境或预发环境真实触发一次微信支付回调拿到完整的返回报文。第三步保存响应体。选中这条请求右键Save Response把响应保存成本地JSON文件。记得把保存的文件路径命名得有语义比如/mock-data/wxpay/callback-normal.json不然一段时间后自己都想不起来这个文件是干什么的。第四步Map Local。右键请求选择Map Local把URL匹配规则和本地文件路径填好。匹配规则支持通配符一般用*pay.weixin.qq.com/*就能覆盖所有相关请求。第五步修改数据验证异常分支。把正常返回文件改成错误码、改成超时返回、改成非法签名然后再次触发请求观察系统是否能正确兜底。这套流程的要点是先有真实报文再改造成Mock报文而不是凭空编造数据。真实报文里有很多边界字段和嵌套结构凭空造容易漏字段。3.2 搭建一个可维护的Express Mock服务当Charles满足不了团队共享的动态Mock需求时我建议直接搭一个Mock服务。这里给一个我项目里用过的简化版本用的是Node.js加Express加Mock.js。const express require(express); const Mock require(mockjs); const app express(); const port 3000; // 模拟网络延迟方便前端调试loading态 app.use((req, res, next) { const delay req.query.delay || 300; setTimeout(next, delay); }); // 根据请求参数返回不同数据 app.get(/api/order/:id, (req, res) { const { id } req.params; const { status } req.query; const statusMap { pending: { code: 0, data: { id, status: pending, amount: 199.00 } }, paid: { code: 0, data: { id, status: paid, amount: 199.00, payTime: 2024-06-01 12:00:00 } }, closed: { code: 0, data: { id, status: closed, amount: 0 } } }; const response statusMap[status] || statusMap.pending; // 支持 mock 参数来模拟异常场景mock500、mocktimeout、mocklimit if (req.query.mock 500) { return res.status(500).json({ code: 500, message: Internal Server Error }); } if (req.query.mock timeout) { return; // 不返回任何响应模拟超时 } if (req.query.mock limit) { return res.status(429).json({ code: 429, message: Too Many Requests }); } res.json(response); }); // 使用Mock.js生成随机列表数据 app.get(/api/user/list, (req, res) { const page parseInt(req.query.page) || 1; const pageSize parseInt(req.query.pageSize) || 10; const data Mock.mock({ list|10: [{ id|1: (page - 1) * pageSize 1, name: cname, phone: /^1[3-9]\d{9}$/, status|1: [0, 1, 2] }], total: 35 }); res.json({ code: 0, data: { ...data, page, pageSize } }); }); app.listen(port, () { console.log(Mock server listening at http://localhost:${port}); });这套服务有几个设计思路值得参考延迟中间件是最容易被忽略但实用性最高的配置。前端在联调阶段很需要校验loading状态、防重复点击这些逻辑如果没有延迟还没看到loading就出结果了等于没测。通过query参数控制返回状态是为了让前端同学可以在不同状态间自由切换不需要麻烦后端改代码。联调时口头约定好status和mock参数的含义就行。第三个思路是错误模拟也要做500、超时、限流这些异常分支不能只靠真实环境碰运气Mock服务可以主动制造这些场景把测试前置。3.3 接口测试代码里的Mock实践粒度与断言除了工具层面的Mock代码里的Mock同样重要。这里的Mock指的是在测试代码中替换掉真实的函数或模块依赖让被测对象只专注自己的逻辑。以Node.js技术栈为例Jest里最常用的是jest.mock// userService.test.js jest.mock(../apiClient, () ({ fetchUser: jest.fn() })); const { fetchUser } require(../apiClient); const { getUserProfile } require(../userService); test(should return user profile when fetchUser succeeds, async () { fetchUser.mockResolvedValue({ id: 1, name: Alice }); const profile await getUserProfile(1); expect(profile.name).toBe(Alice); }); test(should throw error when fetchUser fails, async () { fetchUser.mockRejectedValue(new Error(network error)); await expect(getUserProfile(1)).rejects.toThrow(network error); });用Python写接口测试的话对应的就是unittest.mock.patch# test_order_api.py from unittest.mock import patch patch(services.payment_service.pay) def test_create_order_success(mock_pay): mock_pay.return_value {transaction_id: T20240601001, status: success} result create_order({product_id: 1, amount: 199}) assert result[status] success mock_pay.assert_called_once()代码级Mock最容易犯的错是Mock粒度太大。有人为了省事把整个Service层一次性Mock掉结果被测代码几乎没有真实逻辑被执行测试变成了纯粹的自娱自乐。我的经验是Mock尽量放在边界位置——外部API调用、数据库访问、消息队列发送、文件读写。被测对象内部自己的业务逻辑不要Mock要让它在真实状态下跑。另外Mock返回值的设计要有业务含义不能随便填。比如年龄字段如果业务上不允许负数Mock数据就别返回负数否则测试覆盖的意义有限。好的Mock数据来自真实业务场景而不是随意的占位数据。4. Easy Mock这类公共服务挂了怎么办临场替代与自托管方案4.1 “Easy Mock上不去”的真实原因与排查思路热搜词里“easy mock上不去”这个话题看来是戳中了不少人的痛处。根据我遇到的情况和群里的讨论Easy Mock访问不了的原因通常有这样几类第一类是域名解析问题。免费公共服务经常调整DNS或迁移服务器本地DNS缓存没刷新就会出现解析失败。排查方法是先ping easymock.com换成你实际用的域名看能不能解析出IP然后nslookup查看DNS响应是否正常。第二类是服务商限流或被攻击。免费服务扛不住大流量时会出现间歇性不可用。这种没有太好办法只能等恢复或者临时换工具。第三类是平台本身停止维护。很多免费Mock平台火了几年后没人维护证书过期、依赖的数据库挂了、仓库不再更新最终变成“能打开但注册不了、接口创建失败”的半瘫状态。排查思路简单说就是先分清楚是网络问题、服务端问题还是本地配置问题再决定是等、是换工具还是直接跑本地方案。4.2 临时替代用JSON Server三分钟起一个Mock接口Easy Mock挂掉的时候最稳妥的替代方案就是本地起一个JSON Server。这个工具特别适合临时顶班因为它支持直接把一个JSON文件暴露成RESTful API。安装和启动非常简单npm install -g json-server # 创建 db.json # { # users: [ # { id: 1, name: 张三, role: admin }, # { id: 2, name: 李四, role: editor } # ], # orders: [] # } json-server --watch db.json --port 8080 --delay 500启动后http://localhost:8080/users会自动返回用户列表http://localhost:8080/users/1返回单条数据。JSON Server天然支持POST添加、PUT更新、DELETE删除完美满足Demo和联调需求。--delay 500是我特别标注的参数加上它会在每个请求前延迟500毫秒模拟真实网络环境避免前端在“秒回”环境下开发完到了真实联调阶段才发现loading逻辑根本没写。如果JSON Server默认的REST规范满足不了需求可以通过配置文件添加自定义路由和中间件// server.js const jsonServer require(json-server); const server jsonServer.create(); const router jsonServer.router(db.json); const middlewares jsonServer.defaults(); // 自定义路由 server.use(middlewares); server.get(/api/report/summary, (req, res) { res.json({ total: 126, increaseRate: 0.23 }); }); server.use(router); server.listen(8080, () { console.log(JSON Server is running); });4.3 长期方案把Mock数据纳入代码仓库和CI流程临时顶过了几次班之后我越来越确定一件事Mock数据应该跟代码走而不是跟着某个平台走。把Mock数据纳入代码仓库最简单的方法就是在项目里建一个mock/目录把JSON Server需要的db.json、自定义路由脚本、Mock.js模板都放进仓库。然后给package.json加一个启动脚本{ scripts: { mock: node mock/server.js, dev:mock: cross-env VUE_APP_USE_MOCKtrue vue-cli-service serve } }这样做的收益是第一Mock数据可以版本化。接口结构调整时Mock数据跟着代码评审一起改不会出现“代码已经改了Mock数据还是旧的”这种问题。第二新成员入职拉代码就能跑起来不需要申请某个平台账号。第三配合CI可以在测试流水线里先启动Mock服务再跑前端构建或接口测试实现完全可控的自动化验证。我把这个方案在团队里推了之后最大的变化是联调前大家已经在用同一套Mock数据做过一轮自测了真正联调时的问题数量肉眼可见地少了。5. 进阶玩法动态Mock、异常注入与契约联调5.1 动态Mock根据请求参数返回不同数据很多场景里Mock数据不是固定的而是要跟着请求参数走。比如权益列表要根据用户等级返回不同内容商品详情要根据商品ID返回不同规格。这些用Mock.js的固定模板做不到需要在Mock服务里写动态逻辑。最简单有效的模式就是前面章节里给过的statusMap思路——维护一个数据字典根据查询参数返回对应状态。稍微复杂一点的分页场景用Mock.js生成列表时动态计算起始序号就行。再复杂一些比如需要管理一批“用户会话状态”的场景可以在Mock服务里维护一个内存对象模拟登录、注册、登出的状态流转。let currentUser null; app.post(/api/login, (req, res) { const { username, password } req.body; if (username admin password 123456) { currentUser { id: 1, name: 管理员, role: admin }; return res.json({ code: 0, data: currentUser }); } return res.status(401).json({ code: 401, message: 用户名或密码错误 }); }); app.get(/api/profile, (req, res) { if (!currentUser) { return res.status(401).json({ code: 401, message: 未登录 }); } res.json({ code: 0, data: currentUser }); });这套“状态化Mock”在前端开发里价值很大登录态判断、权限切换、会话过期这些业务流程用普通的抓包改包工具根本模拟不了但是在Mock服务里只是几十行代码的事。5.2 异常注入超时、500、限流、网络抖动不许跳过接口测试里最容易偷懒的是异常场景。正常返回大家都记得测超时、服务端错误、限流这些分支经常被“以后再说”带过。但是对于一个生产系统来说异常分支的容错设计往往比正常流程更体现质量。Mock场景下异常注入可以做得非常细致异常类型模拟方式验证目标接口500Mock服务返回500状态码前端错误提示、日志上报请求超时Mock服务不返回任何数据超时重试机制、loading状态结束限流429Mock服务返回429并带Retry-After头降级策略、排队提示网络抖动随机延迟1-5秒多请求并发时的时序处理数据格式错误返回缺失字段或错误类型前端防御性代码是否生效以超时模拟为例很多前端有“请求最长时间5秒”的设计Mock服务要做的是把这个场景稳定制造出来。我上文的示例代码里mocktimeout就是不调用res返回让请求悬挂直到超时。前端如果设置了超时时间配合这个场景就能验证超时提示和重试逻辑如果没有设置超时这个问题在真实环境里可能要等到用户反馈才会发现。数据格式错误是另一个高频且高价值的异常注入方向。接口返回时少了一个字段、返回的数组在某条件下变成对象、金额从数字变成字符串……这些异常如果在前端没有做类型防御往往会在某个深夜变成线上事故。Mock服务可以在模板里设计一些“畸形数据”分支定期跑一遍看看前端能不能兜住。5.3 向契约测试演进Mock数据与真实接口如何保持一致Mock用得越多就越会面对一个灵魂拷问Mock数据和真实接口不一致怎么办联调阶段一大半的问题都出在这个地方。后端接口返回的字段是userIdMock数据里写的是id后端返回的时间格式是时间戳Mock数据里写的是字符串。前端用Mock数据开发了一个月联调那天发现全部对不上一夜回到解放前。解决这个问题接合契约测试Consumer-Driven Contract是最有效的方式。核心思路是调用方前端把接口的请求和响应规范定义成契约文件服务方后端在开发时按照契约文件实现两端都通过契约校验后再联调。具体到落地可以先用OpenAPI/Swagger定义接口规范然后让Mock服务基于OpenAPI文件生成数据。这样Mock数据的字段结构、类型、必填项跟真实接口是同一份定义天然一致。Apifox这类工具已经内置了从接口定义生成Mock数据的能力用的就是同一套Schema。对于团队还没上OpenAPI的情况退一步的做法是Mock服务的数据模板和真实接口的响应结构必须由同一个后端同学维护前端改Mock数据时要求先找后端确认字段。虽然笨但比两边各写各的强得多。最后几点个人心得做了一年Mock体系建设最深的体会是Mock工具谁都能用但Mock策略拉开差距。第一Mock数据一定要进代码仓库。平台可能挂、工具可能弃坑、同事可能离职唯一靠得住的是代码仓库里的那份数据定义。第二Mock方案要设计“退出机制”。Mock是开发阶段的工具不是生产环境的依赖。我在服务代码里会加环境变量开关只有显式开启Mock模式时才启用Mock逻辑避免哪天忘了关带着Mock代码上了生产。第三团队内Mock的使用规范要用文档固定下来。哪个环境用Mock、哪个接口可以Mock、Mock数据由谁维护、异常场景怎么约定这些都是协作层面的事。工具解决执行效率规范解决协作效率。这些东西不一定能让你写出更炫的代码但一定能让你在下一次联调时少加几天班。
返回列表