引言
你有没有遇到过这种局面?周三上午,后端同学在群里丢一句:”登录接口周五才能给,再等等。”你打开测试计划表,周一周二已经空了两天
眼看着迭代周期一天天逼近,你却只能对着空白的用例干坐着。前端卡在联调,后端卡在自测,测试卡在”没东西可测”。三方互相干等,时间就这么流走了
其实方案一直就在手边——Mock。但请注意,Mock 从来不是”造假数据”这么简单。它的核心价值是解除依赖。当后端接口还没就绪,你用 Mock 把契约层先搭起来,测试活动就能独立往前推进
今天要聊的 WireMock,就是一个可编程的 HTTP 模拟服务器。它不依赖任何外部服务,跑在本地或容器里,按你定义的规则,对每一次请求返回你设定的响应。后端没交付?没关系,你自己造一个接口先用着
|
✍ 我的经历:这条规矩改变了整个团队节奏 刚带测试团队那几年,最怕排期会上那句”接口还得两周”。两周里测试做什么?手工点旧版本?那测出来的东西跟新需求对不上。 后来我跟团队定了一条规矩:需求评审一结束,测试就根据接口文档把 Mock 搭起来。等开发把真接口提上来,我们的用例早跑过三轮了。前后端联调那天,反而成了轻松的一天——契约已经在用真实接口验证一遍了,有什么问题当场发现当场改。 这条规矩后来成了团队的默认工作流。新人入职第一周就会问:”Mock 搭了吗?”大家慢慢意识到,测试不是等开发给东西,而是主动去接东西。 |
01WireMock是什么:一个”听话的演员”
1.1 核心概念:Stub 与 Mapping
把 WireMock 想象成剧场里一位特别听话的演员。你给他一份”剧本”,他就照着演。这份剧本,在 WireMock 里叫 Stub(存根)。
一个 Stub 描述了一件事:当收到某种请求时,返回某种响应。比如”收到 POST /api/login,就返回 200 和一段 token”。请求长什么样、响应长什么样,全部由你写死,或动态生成。
那么 Mapping(映射) 又是什么?它是一组 Stub 的集合,通常对应一个 JSON 文件。你在 mappings 目录下放一个 login.json,里面定义好登录相关的 Stub,WireMock 启动时就会加载它。
可以理解为:Stub 是单条规则,Mapping 是装这些规则的”文件抽屉”。一个文件里能放多个 Stub,按优先级和匹配度依次尝试。
你可能会问,多个 Stub 都匹配同一个请求怎么办?WireMock 有一套优先级机制。每个 Stub 可以设 priority,数字越小越优先;优先级相同时按匹配精确度排序,匹配维度越多越精确。理解这一点很重要,不然你写多了 Stub 就会发现”为什么总命中那个不是我想要的”。所以写 Mock 和写用例一样,约束要写全,别留模糊地带。
1.2 三种运行模式
WireMock 提供了三种常见跑法,适应不同场景。
方式一是 Standalone(命令行)。你下载一个独立 jar 包,敲一条 java -jar 命令就能起服务。适合本地快速验证,随手就能用,不掺和任何容器。
方式二是 Docker(容器化)。拉官方镜像 wiremock/wiremock,一条 docker run 把服务跑在容器里。好处是环境干净、可复用、方便团队共享同一套 Mock 配置。这也是我在团队里推荐的做法。
方式三是 嵌入式(Embedded)。在 Java 项目里通过依赖把 WireMock 嵌进去,随测试一起启动和销毁。适合写 Java 集成测试的同学,跟 JUnit 配合得很顺。
1.3 和 Postman Mock 的区别
很多同学常见反应是:”Postman 不是也能 Mock 吗?”能,但差别很大
Postman Mock 需要登录账号、依赖云端,配置分散在个人工作区里,团队协作时容易各搞各的。更关键的是,它不支持状态管理——你没法让同一个接口在不同调用次数下返回不同结果
它也不支持故障注入,想模拟一个 500 错误,得手动改配置。WireMock 完全本地运行,不绑定任何账号。它支持状态机(Scenarios),能让接口按步骤切换响应;支持延迟注入,能模拟慢接口;支持随机、代理、优先级匹配等玩法
再补一句:有人会拿 WireMock 跟单元测试里的 Mock 框架比。那是两个层面。Mock 框架(比如 Java 的 Mockito)Mock 的是代码里的对象,跑在进程内;WireMock Mock 的是 HTTP 服务,进程外、跨语言。前端页面、测试脚本、别的语言写的服务,都能直接调它。这正是它适合做接口契约测试的根本原因:它站在真实的网络协议层,而不是代码层
1.4 适用人群
谁会用得上?
接口测试工程师:不用干等后端,提前把契约测完。前端开发:接口没好也能调页面,联调效率翻倍。微服务测试:在复杂调用链里,把下游服务全部 Mock 掉,专注测当前服务。
如果你属于这三类,这篇文章值得看到末尾。顺带说,不只是这三类。性能测试同学也能用 WireMock 当稳定的下游,排除被测服务之外的变量;技术文档同学能用它快速造出可演示的接口;甚至产品经理都能用它搭个能点的 demo 去跟老板汇报。只要你的工作卡在”某个 HTTP 接口还没好”,它就有用武之地。门槛不高,回报却不小。
图片引自微信公众号,扫码关注阅读原文02Docker三分钟搭建Mock服务
2.1 一条命令启动
省事的方式是 Docker。确保你的机器装了 Docker,然后执行下面这条命令:
# 后台启动 WireMock,把容器 8080 端口映射到本机 8080docker run -d --name wiremock -p 8080:8080 wiremock/wiremock
代码 1:docker run 一键拉起 WireMock 服务
这条命令拉取官方镜像并后台启动。几秒钟后,服务就活了。你可以用 curl 或浏览器探一下 http://localhost:8080 看是否就绪。
如果想用独立 jar 而不是 Docker,去官网下 wiremock-standalone 的 jar 包,执行 java -jar wiremock-standalone-版本号.jar –port 8080 –verbose 同样能起。两者底层一致,区别只在运行环境。新手我建议从 Docker 入手,少踩环境坑,尤其是 Windows 上配 Java 环境变量那一套,能省则省。
起完服务后,顺手用 docker ps 确认容器状态是 Up,再用 curl http://localhost:8080/__admin/mappings 看一眼已加载的映射列表。如果列表为空,说明 mappings 目录没挂对;如果连不上,先查端口是否被占用。这几个命令我每天都要敲,熟练之后排错基本在十秒内完成。
2.2 访问管理后台
服务起来后,打开浏览器访问 http://localhost:8080/__admin/。这是 WireMock 的管理后台,用来看当前加载了哪些 Stub、匹配了多少次请求、近期的调用日志。
后台里有个功能我几乎天天用:查看单个 Stub 的匹配次数和近期请求体。当你觉得”我明明配了啊怎么没返回”,点开近期请求,往往发现是请求头少了个字段,或 URL 多带了斜杠。这类问题靠猜半天不如看一眼日志。另外,后台还能直接增删改 Stub,紧急调试时不用改文件重启,很方便。
|
✍ 一次真实的排障经历 有一次团队里的小陈跟我说:”登录 Mock 怎么调都不通,一直返回 404。”我过去一看,他配的是 POST /api/login,但实际前端发的是 POST /api/login/?from=app——多了一个 query 参数。 他用的是 urlPath 精确匹配,不会忽略 query,所以两个请求根本不是同一个。我让他把配置改成 urlPath(只匹配路径部分),或者干脆用 urlPathPattern 加正则排除 query,问题立刻解决了。 后来我把这件事记了下来,在团队 code review 清单里加了一条:”urlPath 还是 url?查清楚再配。”类似的小坑踩过一次,后面七八个人都跟着避开了。 |
2.3 目录结构与 docker-compose 挂载
WireMock 有两个关键目录。/mappings 放 JSON 映射文件,每个文件定义若干 Stub。/__files 放响应模板或静态文件,比如你要返回的 HTML、大段 JSON。
用 Docker 时,记得把这两个目录挂载出来,否则容器一删,你写的配置全没了。稳妥的做法是 docker-compose 挂载本地目录:
version: "3.8"services:wiremock:image: wiremock/wiremock:latest # 官方镜像,跟随稳定版container_name: wiremock # 容器名,方便 docker 命令引用ports:- "8080:8080" # 宿主机 8080 映射到容器 8080volumes:- ./mappings:/home/wiremock/mappings # 挂载映射目录,热加载 Stub- ./__files:/home/wiremock/__files # 挂载响应文件目录command: ["--verbose"] # 开启详细日志,方便排查匹配失败
配上 compose 之后,你在本地 mappings 目录里改文件,WireMock 会自动热加载,不用重启容器。这对边写边调的节奏很友好
03基础——Mock首个接口
3.1 JSON 映射文件
我们亲手 Mock 首个接口:登录。在 mappings 目录下创建 login.json:
{"request": {"method": "POST", // 只匹配 POST 请求"urlPath": "/api/login" // 精确匹配路径,忽略查询参数},"response": {"status": 200, // 返回状态码 200"headers": {"Content-Type": "application/json; charset=utf-8"},"jsonBody": { // 直接写 JSON 体,WireMock 自动序列化"code": 0,"msg": "success","data": {"token": "mock-token-2026-abc123","userId": 10086,"expireIn": 7200}}}}
// 注释即为可运行配置;下文 Python 代码则保留 # 注释,二者均完整可运行。 |
这个 Stub 的意思是:凡是 POST 到 /api/login 的请求,都返回 200,body 里带一个 token。注意用的是 urlPath 精确匹配路径,不关心查询参数
这里有个细节值得记:urlPath 只比路径,url 比完整地址含 query。如果你的接口靠 query 参数区分,比如 /api/list?page=1,用 urlPath 会忽略 page,导致所有分页都命中同一个 Stub。需要区分时就用 urlPathPattern 配合正则,或者额外加 queryParameters 匹配
还有个常见混淆:response 里用 jsonBody 还是 body?jsonBody 让 WireMock 帮你序列化并自动加 JSON 头,写起来更干净;body 则是你自己写原始字符串,适合返回 XML 或非标准格式。我的经验是,只要返回的是 JSON,一律用 jsonBody,少出错。反之要返回一段 HTML 或纯文本,就用 body 配合手动设置的 Content-Type
3.2 请求匹配规则
WireMock 的匹配规则很灵活,远不止”路径”这一种
URL 匹配有两种:url 精确匹配完整路径,urlPath 匹配路径部分(忽略 query)。还支持 urlPathPattern 用正则匹配,比如 /api/users/[0-9]+ 匹配任意用户 ID
Method 匹配:GET、POST、PUT、DELETE 任选。Header 匹配:可以指定某个请求头必须存在或取值符合预期,比如要求 Content-Type 是 application/json
Body 匹配:强大的是 JSONPath 和 XPath。你可以写 “$.username” 去匹配请求体里的某个字段,实现”不同 username 返回不同结果”。这一招在第四章会大显身手
匹配规则还能组合。一个 Stub 可以同时要求 method、header、body 三处都满足才命中。匹配越精确,Stub 之间越不容易打架。我的建议是:写 Mock 时也像写用例一样严谨,把请求约束写全,后面排查省心。反过来,如果某个 Stub 永远不被命中,多半是约束写得太死,或者跟另一个更高优先级的 Stub 抢了匹配
3.3 用 Python 验证
光写 Stub 不够,得验证它真能用。我用 Pytest + Requests 写个轻量验证:
import requestsimport pytest# 基础地址:指向本地 WireMock 容器BASE_URL = "http://localhost:8080"def test_login_success():"""验证登录接口能正常返回 token"""payload = {"username": "tester","password": "123456"}# 调用 Mock 出来的登录接口resp = requests.post(f"{BASE_URL}/api/login", json=payload, timeout=5)# 断言状态码assert resp.status_code == 200body = resp.json()# 断言业务字段存在且类型正确assert body["code"] == 0assert "token" in body["data"]assert isinstance(body["data"]["token"], str)assert len(body["data"]["token"]) > 0def test_login_response_time():"""验证接口响应在合理时间内返回(无延迟配置时)"""resp = requests.post(f"{BASE_URL}/api/login", json={}, timeout=5)assert resp.elapsed.total_seconds() < 1.0
跑起来,绿了,说明 Mock 接口已就绪。从这一刻起,你的登录相关测试用例,不再依赖后端那根还没写好的接口
这里有个小技巧:把 BASE_URL 抽到配置文件或环境变量,别硬编码在用例里。这样同一条用例,测试环境指向 WireMock,联调环境指向真实服务,一行不改就能切换。后面第七章坑2 会再强调这件事,因为它确实容易忘。另外,断言别只写 status_code == 200,多断言几个业务字段,Mock 的契约价值才真正发挥出来
顺便提一句断言的粒度。Mock 出来的接口,断言要恰到好处:太粗(只判断 200)测不出契约问题,太细(连 token 字符串都写死比对)又会让用例脆弱、一改就红。我的习惯是断言状态码、关键业务字段的存在性与类型,不纠结具体随机值。这样 Mock 既能帮你守住契约,又不会因为无关细节频繁失败
04进阶——动态响应与状态管理
4.1 Response Templating
上面那个登录接口,无论谁登录都返回同一个 token。真实场景里,我们常需要根据请求内容动态生成响应。这就用到 Response Templating
WireMock 内置 Handlebars 模板引擎。你在响应里写 {{…}} 占位符,它会在返回时替换成实际值。比如根据 username 不同,返回不同的 code;用 {{randomValue}} 生成随机 token
{"priority": 1, // 优先级,数字越小越先匹配"request": {"method": "POST","urlPath": "/api/login"},"response": {"status": 200,"headers": { "Content-Type": "application/json" },"jsonBody": {"code": "{{#eq request.body.username 'admin'}}0{{else}}1{{/eq}}","msg": "{{#eq request.body.username 'admin'}}success{{else}}invalid user{{/eq}}","data": {"token": "{{randomValue length=20 type='ALPHANUMERIC'}}"}},"transformers": ["response-template"] // 开启 Handlebars 模板引擎}}
注意末尾的 transformers: ["response-template"],这是开启模板引擎的开关。没有它,{{…}} 会被当成普通字符串原样返回,你就看不到动态效果了
Handlebars 的能力不止随机值。它能取请求里的 header、query、body 字段,能做条件判断、循环、字符串处理。比如按请求的城市字段,从一份大 JSON 里筛出对应城市的天气返回。配合 __files 目录里的模板文件,你甚至能返回一整页渲染好的 HTML。换句话说,只要你的响应逻辑能用模板表达,WireMock 都能替你演出来,而不必写一行服务端代码
提醒一个顺序问题:如果你同时开了 response-template 和别的 transformer,transformers 数组里的顺序会影响执行先后。大多数场景下你只需要 response-template,不必叠加
4.2 场景:登录成功 / 失败 / 过期
同一接口按条件返回不同结果,常见于”登录态”测试。比如:username 是 admin,返回成功;是 locked,返回账号锁定;是 expire,返回 token 过期。用 Handlebars 的 eq 判断就能实现分支
这种”按输入返回不同输出”的能力,让你一个 Stub 覆盖多种用例,不用为每个分支单独建文件。维护成本一下子降下来
不过要克制。动态模板写得太花,Mock 本身就成了需要维护的逻辑。我的原则是:动态部分只服务于”让测试更真实”,不为炫技。能用固定值表达的,就别上模板。毕竟 Mock 是手段不是目的,它该帮你早点测完,而不是变成另一个要测的系统
4.3 状态机:多步骤流程
更复杂的场景是”有先后顺序”的接口。比如电商下单:没加购就去下单,应该报错;加购之后才能下单;下单之后才能支付
这种步骤依赖,靠 Scenarios(状态机) 实现。每个 Stub 声明自己属于哪个场景、当前需要什么状态、命中后切换到什么状态。WireMock 会按调用顺序维护状态
{"scenarioName": "ShoppingFlow", // 状态机名称,多个 Stub 共享"requiredScenarioState": "Started", // 当前状态下才匹配"newScenarioState": "CartAdded", // 匹配后迁移到新状态"request": {"method": "POST","urlPath": "/api/cart/add"},"response": {"status": 200,"jsonBody": {"code": 0,"msg": "added to cart","data": { "cartId": "C1001", "count": 1 }}}}
上面这个 Stub 属于 ShoppingFlow 场景,要求当前状态是 Started,命中后把状态切到 CartAdded。后续的”下单”Stub 就可以要求状态必须是 CartAdded 才匹配——没加购直接下单,自然匹配不到,返回你预设的失败响应
状态机的默认值叫 Started,所有 Stub 初始都处在 Started。你可以定义任意多个状态名,串成任意长的流程。但要注意:状态是 WireMock 实例级别的全局状态,多个用例并行跑时可能互相干扰
4.4 用 Pytest 跑端到端流程
把状态机串起来,就是一条端到端链路。我用 Pytest 写一个完整流程验证:
import requestsimport pytestBASE_URL = "http://localhost:8080"# 重置状态机接口,保证每次用例从 Started 开始SCENARIO_RESET = "http://localhost:8080/__admin/scenarios/reset"def test_state_transition():"""验证状态机:未加购直接下单应失败"""requests.post(SCENARIO_RESET) # 先重置,避免上次用例残留状态# 未加购,直接调下单接口,期望匹配不到(返回 404)r = requests.post(f"{BASE_URL}/api/order/create",json={"cartId": "C1001"}, timeout=5)assert r.status_code == 404 # 状态不对,Stub 未命中# 先加购,状态切到 CartAddedr2 = requests.post(f"{BASE_URL}/api/cart/add",json={"productId": 1, "count": 1}, timeout=5)assert r2.status_code == 200# 再加购后下单,期望成功r3 = requests.post(f"{BASE_URL}/api/order/create",json={"cartId": "C1001"}, timeout=5)assert r3.status_code == 200
|
✍ 经验:CI 上偶发失败原来是状态机捣的鬼 有段时间,CI 上的用例偶发失败,本地复现不了。查了三四天,最后发现是前一天晚上有人手动调了一次支付接口,把状态停在了 Paid。第二天首批用例从错误的状态开始,后续的状态流转全部错位。 解决方法是:在每个用例开头加 reset,而不是结尾——结尾 reset 解决不了下一个用例开头的问题。之后世界清净了。所以记住:凡用状态机的自动化,reset 不是可选项,是必选项。 |
05实战——Mock一个完整电商下单流程
5.1 接口清单
我们挑一条完整的电商下单链路,一共 5 个接口:
-
商品列表:GET /api/products -
商品详情:GET /api/products/{id} -
加入购物车:POST /api/cart/add -
下单:POST /api/order/create -
支付:POST /api/pay
这 5 个接口串起来,覆盖了”看—选—加—买—付”的完整用户路径。后端没写?我们用 WireMock 把整条链 Mock 出来,测试照样能跑
挑这条链是有用意的:它既有读(列表、详情),又有写(加购、下单、支付),还跨了多个状态。一条用例就能覆盖”读-写-状态”三类行为。你拿自己项目的任意核心链路套这套方法都行,不必是电商。订单流转、审批流、注册登录流,本质都一样——把它们拆成”接口节点 + 状态流转”,WireMock 就能完整演一遍
还有一点很实用:这条链路造出来之后,你不止能测功能,还能拿它做演示和压测基线。比如给前端同学一个稳定的”假后端”,他不必等任何人就能调页面;给性能同学一个可控的下游,排除外部变量。一个 Mock 链路,常常能同时服务测试、前端、性能三方,投入产出比很高
5.2 Mock 映射文件
5 个 JSON 映射文件,分别放在 mappings 下。商品列表用模板随机生成商品;加购触发状态迁移;下单要求 CartAdded 状态;支付要求 Ordered 状态;支付成功后切到 Paid
下面把”商品列表”和”支付”两个贴出来,其余结构类似,按需组合状态机即可:
{"request": { "method": "GET", "urlPath": "/api/products" },"response": {"status": 200,"jsonBody": {"code": 0,"data": [{ "id": 1, "name": "{{randomValue type='ALPHANUMERIC' length=6}}", "price": 99 },{ "id": 2, "name": "{{randomValue type='ALPHANUMERIC' length=6}}", "price": 199 }]},"transformers": ["response-template"]}}
{"scenarioName": "ShoppingFlow","requiredScenarioState": "Ordered", // 必须已下单"newScenarioState": "Paid", // 支付后切到 Paid"request": { "method": "POST", "urlPath": "/api/pay" },"response": {"status": 200,"jsonBody": { "code": 0, "data": { "paid": true, "orderId": "{{request.body.orderId}}" } },"transformers": ["response-template"]}}
5.3 Pytest 测试脚本
把 5 个接口全串起来,写成一条端到端用例:
import requestsimport pytestBASE_URL = "http://localhost:8080"SCENARIO_RESET = "http://localhost:8080/__admin/scenarios/reset"def test_full_order_flow():"""端到端:浏览 -> 加购 -> 下单 -> 支付"""# 步骤0:重置状态机,保证每次从 Started 开始requests.post(SCENARIO_RESET)# 步骤1:商品列表r1 = requests.get(f"{BASE_URL}/api/products", timeout=5)assert r1.status_code == 200product_id = r1.json()["data"][0]["id"]# 步骤2:商品详情r2 = requests.get(f"{BASE_URL}/api/products/{product_id}", timeout=5)assert r2.status_code == 200# 步骤3:加入购物车(触发状态 CartAdded)r3 = requests.post(f"{BASE_URL}/api/cart/add",json={"productId": product_id, "count": 1}, timeout=5)assert r3.status_code == 200cart_id = r3.json()["data"]["cartId"]# 步骤4:下单(需要 CartAdded 状态)r4 = requests.post(f"{BASE_URL}/api/order/create",json={"cartId": cart_id}, timeout=5)assert r4.status_code == 200order_id = r4.json()["data"]["orderId"]# 步骤5:支付(需要 Ordered 状态)r5 = requests.post(f"{BASE_URL}/api/pay",json={"orderId": order_id, "amount": 99.0}, timeout=5)assert r5.status_code == 200assert r5.json()["data"]["paid"] is True
这个脚本把 5 个接口全串起来了。注意三件事:状态重置、数据传递、逐步断言。任何一步断掉,测试立刻红,你能精准定位是”接口契约”还是”调用顺序”出了问题
这条端到端用例的价值,不只是”证明接口通”。它像一份可执行的接口文档:新同学看这个脚本,就知道正确调用顺序是什么、每一步要传什么字段、字段从哪一步来。契约一旦变了,脚本先红,比人肉 review 文档靠谱得多。我甚至把它当入职培训材料用——新人照着脚本跑一遍,比看三页接口文档学得都快
5.4 报告生成
团队用的话,光有 print 不够。我接入 Allure,把每次请求和响应作为附件挂到报告上。这样用例失败时,点开就能看到当时 Mock 返回了什么,排查效率很高
做法也很轻:在请求前后用 allure.attach 把 request/response 内容以 text 形式挂上去。WireMock 本身不依赖 Allure,二者是解耦的,你想换 pytest-html 同样可行
补充一句:Allure 的附件不只挂响应,也可以挂请求。出问题时,请求和响应对照着看,定位更快。报告里我通常还会在用例收尾加一步”调完 reset”,保证每个用例独立,避免连锁污染。当用例数量上去之后,这种”各自干净”的纪律,比任何调试技巧都重要
当链路变长、用例变多,我还会把”重置状态机”抽成一个 pytest fixture,每个用例自动套用,不用每次手写 reset。配合 conftest.py 统一 BASE_URL 的来源,整个测试工程的入口就收敛到一处,新人接手也不会把环境配错。工程化这一步,是从”能跑”到”好维护”的分水岭
图片引自微信公众号,扫码关注阅读原文06高阶——故障模拟与延迟注入
6.1 延迟响应
前端怕的不是接口报错,是接口”又慢又不确定”用 fixedDelayMilliseconds 让接口 3 秒后才返回,专门测前端的 loading 动画、超时提示、重试逻辑
{"request": { "method": "GET", "urlPath": "/api/products" },"response": {"status": 200,"fixedDelayMilliseconds": 3000, // 延迟 3 秒返回,测前端 loading"jsonBody": { "code": 0, "data": [] }}}
延迟还能配合前端埋点一起测。比如要求接口超过 1 秒就上报一次”慢请求”监控,你用 Mock 设 1200 毫秒延迟,就能稳定触发这条监控路径。真实环境想造这么准的延迟,几乎不可能。所以故障模拟不是锦上添花,它是唯一能稳定复现”偶发慢”的手段。把慢接口纳入用例,前端的加载态、骨架屏、超时兜底,才真能被验证到
延迟还有随机玩法:除了 fixedDelayMilliseconds 固定延迟,你可以用 delayDistribution 配置一个随机区间,比如 500 到 2000 毫秒之间浮动。这样更接近真实网络的抖动,前端 loading 和超时逻辑在”有时快有时慢”下才暴露真实表现。固定延迟适合测明确的超时阈值,随机延迟适合测鲁棒性,两者都备着更好
6.2 模拟错误状态码
把 status 改成 500、403、429,测系统对各类错误的兜底。比如 429 限流,看前端有没有友好的”操作频繁”提示,而不是白屏;500 时有没有重试或降级
我特别建议把 429 和 503 也纳入。这两类在流量高峰和发布期间常见,却容易被忽略。前端对 429 的处理(退避重试、友好提示)一旦没写好,用户体验会断崖式下跌。而这类错误在测试环境几乎不会自然出现,你不主动 Mock,就永远测不到。所以错误码不是顺手加的,是必须列进用例清单的
6.3 模拟网络中断
WireMock 还能返回空响应、断连、返回损坏的 body。这些在真实网络里偶尔发生,但真实环境很难稳定复现。Mock 让你随时一键触发,把异常路径也纳入日常测试
具体做法:用 response 直接返回空 body,或设一个极短超时让连接中断,或返回一段非 JSON 的脏数据测解析容错。这些在真实环境里”看运气才碰到”,Mock 让你把它们变成”每天必跑”。我见过太多系统,正常流程稳如老狗,一遇到脏数据就整个崩掉——根因就是没人测过异常路径。Mock 的价值,一半在 happy path,另一半恰恰在这些麻烦事上
6.4 批量故障场景
你可以一次性定义多种故障模式,用随机或轮询策略返回。配合自动化,每次构建随机抽一种故障注入,相当于给系统做”混沌演练”,提前暴露脆弱点
经验之谈:把故障模式写成一组 Stub,用 priority 和随机策略切换。这样同一套自动化,每天跑出来的”坏情况”都不一样,覆盖更立体。不过别贪多。故障模式太多,反而掩盖重点。我一般维护 5 到 8 个典型故障(慢、500、403、429、空响应),覆盖住主要风险就够。剩下的交给真实环境的灰度观察,Mock 和真实环境各司其职,才是一个健康的质量体系
图片引自微信公众号,扫码关注阅读原文07实战:支付网关Mock完整案例
7.1 背景:支付接口不稳定,测试被反复阻塞
去年Q3,我们团队接了一个电商系统重构项目,核心链路之一是支付。支付依赖第三方支付宝/微信接口,而第三方接口在测试环境有严格的风控限制——每天只能调有限次数,而且响应时间不稳定,经常 3-5 秒才回来,偶发超时
问题是:支付接口一慢或一超时,我们的集成测试就开始随机失败。每次 CI 跑出来红色,团队第一反应是”是不是第三方接口又不稳定了”,查半天发现是测试环境网络抖动,根本不是代码问题。这种”狼来了”的情况持续了两周,大家对红色警报的敏感度严重下降,真正的问题反而被淹没了
|
✍ 转折点:一次周五下午的”支付测试大堵塞” 最严重的一次是周五下午,临近上线节点。第三方支付突然开始大量超时,CI 上十几条支付相关用例全部红掉。前端同学没法调页面,后端在等接口,测试在等两边都稳下来再跑,三方同时卡住。 当时我紧急把所有支付相关的用例改成 Mock 模式,用 WireMock 把第三方接口 Mock 掉,切换 BASE_URL,一个小时后 CI 重新跑起来,红色消失。那次之后我在团队例会上正式提出:支付链路的第三方依赖必须全部 Mock,这是测试稳定性的基础设施,不是可选项。 团队一致同意,之后再也没有因为第三方接口不稳定导致 CI 失败的情况。 |
7.2 Mock 设计方案
支付网关的 Mock 需要覆盖以下场景:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.3 实现过程
第一步:梳理支付接口契约。跟后端同学要到了第三方支付的接口文档,确认了请求参数(orderId、amount、channel、userId)和响应字段(code、msg、data{paid, transactionId, paidAt})。这是 Mock 的前提——你得知道真实的契约长什么样,才能 Mock 得准
第二步:写 Stub 文件。支付成功 Stub 是整条链路的基础,核心是回填 orderId:
{"scenarioName": "PaymentFlow","request": {"method": "POST","urlPath": "/api/pay/gateway","bodyPatterns": [{ "matchesJsonPath": "$.orderId" }]},"response": {"status": 200,"jsonBody": {"code": 0,"msg": "支付成功","data": {"paid": true,"transactionId": "{{randomValue length=32 type='ALPHANUMERIC'}}","paidAt": "{{now format='yyyy-MM-dd HH:mm:ss'}}"}},"transformers": ["response-template"]}}
{"priority": 1, // 高优先级,优先匹配"request": {"method": "POST","urlPath": "/api/pay/gateway","bodyPatterns": [{ "matchesJsonPath": "$.amount", "greaterThan": 10000 }]},"response": {"status": 200,"jsonBody": {"code": 1002,"msg": "余额不足","data": null}}}
这里用 bodyPatterns 的 greaterThan 匹配金额——当 amount > 10000 时触发余额不足场景。你还可以扩展成精确金额匹配(equalToJson)或金额区间匹配,让不同测试用例命中不同的失败 Stub
7.4 测试执行:六场景全覆盖
六个场景,我写了六条 Pytest 用例,每条对应一个 Stub,通过切换 BASE_URL 或调整请求体触发对应 Mock:
import requestsimport pytestimport osBASE_URL = os.getenv("PAY_BASE_URL", "http://localhost:8080")def test_pay_success():"""支付成功:验证订单状态变更为已支付"""resp = requests.post(f"{BASE_URL}/api/pay/gateway",json={"orderId": "ORD001", "amount": 99.0, "channel": "alipay"},timeout=5)assert resp.status_code == 200body = resp.json()assert body["code"] == 0assert body["data"]["paid"] is Trueassert "transactionId" in body["data"]def test_pay_balance_insufficient():"""余额不足:金额 > 10000 触发余额不足 Mock"""resp = requests.post(f"{BASE_URL}/api/pay/gateway",json={"orderId": "ORD002", "amount": 15000.0, "channel": "wechat"},timeout=5)body = resp.json()assert body["code"] == 1002assert "余额不足" in body["msg"]def test_pay_timeout():"""支付超时:模拟第三方接口延迟 8 秒"""resp = requests.post(f"{BASE_URL}/api/pay/gateway",json={"orderId": "ORD003", "amount": 50.0, "channel": "alipay"},timeout=10)# 如果配置了 8 秒延迟,10 秒内会收到响应(200)# 如果没配置 Mock,返回 504 或超时assert resp.elapsed.total_seconds() > 7.5def test_pay_duplicate():"""重复支付:同一 orderId 第二次支付应返回幂等错误"""# 第一次支付:Mock 会生成 transactionId 并切换状态r1 = requests.post(f"{BASE_URL}/api/pay/gateway",json={"orderId": "ORD004", "amount": 88.0}, timeout=5)assert r1.json()["code"] == 0# 第二次同一订单:Mock 状态机应返回已支付r2 = requests.post(f"{BASE_URL}/api/pay/gateway",json={"orderId": "ORD004", "amount": 88.0}, timeout=5)body2 = r2.json()assert body2["code"] == 1004assert "已支付" in body2["msg"]
7.5 切换到真实环境
Mock 跑通之后,紧接着要做的一步是:切换回真实环境做回归。Mock 只能验证契约,不能验证第三方真实的业务规则(比如风控模型、账户状态)
切换方法很简单:改一行环境变量。BASE_URL 从 http://localhost:8080(Mock)换成 https://pay-api.example.com(真实环境),其他代码一行不动。真实环境跑完之后记得切回来,否则 CI 会拿真实接口跑自动化,第三方账户会扣真实钱
|
✍ 一次差点”真扣款”的教训 有一次联调结束,我忘了把 BASE_URL 切回 Mock,自动化继续跑。结果半夜 CI 执行了一条真实支付用例,用的是测试环境的沙箱账号,本来不会真扣钱——但测试环境的沙箱配置有问题,走了真实支付通道,账户被扣了几块钱。 钱不多,但性质严重。第二天我立刻加了保护:在真实环境 BASE_URL 下,自动化遇到支付类接口直接跳过并打标,绝不自动执行真支付操作。这个保护后来又救了我们一次——有一次 CI 配置错误误指向了生产环境,自动化自动跳过了所有写操作,没有造成任何损失。 教训:环境切换是自动化的命门,写死在代码里不如用环境变量管理,加一层断言保护更稳妥。 |
图片引自微信公众号,扫码关注阅读原文08避坑与行动清单
8.1 四个常见坑
坑1:Mock 数据和真实接口漂移。Mock 写久了,容易和真实接口”长得不一样”。某天后端改了字段名,你的 Mock 还停在旧版,测试全过但上线就挂。对策:定期用真实接口回归,拿真实响应和 Mock 做 diff。
具体怎么做 diff?接口稳定后,抓一份真实响应存下来,跟 Mock 的 jsonBody 逐字段比对。字段名、类型、结构,有任何不一致就标红。我用一个简单脚本每周跑一次,省得人肉对着看。这一步看似麻烦,却能拦住一类隐蔽的”Mock 绿、线上挂”事故,值得常态化。
坑2:忘记切换回真实环境。测试跑完,配置还指向 Mock,结果联调时调的是假接口,排查半天才发现。对策:用环境变量管理 BASE_URL,测试环境和联调环境一键切换。
更稳妥的是在用例层做断言保护:如果检测到当前 BASE_URL 指向真实环境,关键写操作(下单、支付)自动跳过或打标。这样就算忘了切,也不会拿真实环境做破坏性测试。这条尤其重要——我曾见过有人把 Mock 配置留到了生产验证环节,差点对真实库下了单。环境隔离不是君子协定,要写成代码硬约束。
坑3:WireMock 状态丢失。Docker 容器重启,没挂载 volume,所有 Stub 配置蒸发。对策:docker-compose 挂载 mappings 和 __files 目录,配置持久化。
还有个相关的小坑:容器名冲突。第二次 docker run 同名字报已存在,记得先 docker rm 旧的,或干脆用 compose 统一管理生命周期,少手动敲命令。把起停都收进一条 compose 命令,新人照着文档跑也出不了错,这才是团队可复用的样子。
坑4:Mock 响应太简单。只返回 200 和 happy path,真实世界的 500、超时、限流全没覆盖。对策:把第六章的故障模拟纳入日常,Mock 也要”制造麻烦”。
说白了,Mock 不是越绿越好。如果你的 Mock 永远只返回成功,那它验证的只是”代码在理想环境下能跑”。真正的质量,是在各种麻烦里还能稳住。所以别把 Mock 当成让测试变绿的工具,要把它当成帮你想全异常的工具——它逼着你问一句:如果这一步失败了,系统会怎样?
8.2 工具对比
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
表里几个名字,简单说一下定位。Mockito 是单元测试里的对象 Mock,跑在 Java 进程内,适合测某个类的方法逻辑;Mountebank 和 WireMock 类似,也支持多协议(TCP、SMTP 等),但学习曲线更陡;Postman Mock 胜在和接口调试一体,但能力浅。我的选择逻辑很简单:接口契约层要动态、要状态、要故障——选 WireMock,基本够用且顺手。
补充一句选型心态:工具各有特点,没有哪种在所有场景都通吃,关键是看合不合适。如果你的项目已经重度依赖 Postman 做接口管理,先用 Postman Mock 把 happy path 挡上也没问题;等你需要状态机、故障注入这些能力时,再迁移到 WireMock 不迟。关键是别让”等接口”卡住测试,至于用哪个工具,能解决问题就行。
图片引自微信公众号,扫码关注阅读原文