AI图鉴

AI图鉴首页 › AI 资讯 › AI for Testing 提效实战 · 执行自动化(六):轻量 Mock 编

AI for Testing 提效实战 · 执行自动化(六):轻量 Mock 编排实战——WireMock 手工落地 + AI 加速

来源:掘金 · 发布时间:10-25 · 由 AI图鉴每小时自动聚合

一、系统一旦有外部依赖,测试会撞上什么 一个出海广告投放系统,运行时要调一堆不归你控制的东西:Google/Meta/AppsFlyer 的查点击与转化回传、支付、短信、风控,以及公司内别的团队的订单

执行自动化 ·(六)

一、系统一旦有外部依赖,测试会撞上什么

一个出海广告投放系统,运行时要调一堆不归你控制的东西:Google/Meta/AppsFlyer 的查点击与转化回传、支付、短信、风控,以及公司内别的团队的订单、账户、预算服务。于是测试撞上六类问题:

Mock 就是用一个"可控的假依赖"换掉"不可控的真依赖"。

二、Mock 的八个真实痛点

光知道"起个假服务"不够,真正磨人的是下面八条。

- HTTPS 拦不下来:不是工具不支持 HTTPS,而是证书信任链卡人——自签证书客户端默认不信(PKIX path building failed)、mTLS 要客户端证书、App 证书固定(Pinning)+ Android 7+ 不信用户证书连抓包都抓不到、QUIC/HTTP3 绕代理。

- Mock 被动不进化(最危险):后端把 status 改成 state、金额单位从元改分,你的假服务还按旧规则返回,测试一片绿,切真环境就崩。

- 搭起来慢、维护更慢:一条条手写匹配规则和返回体,接口一多就是体力活。

- 有状态、有顺序的场景难配:"前两次 500 第三次成功"(验重试)、"回调发两次账只记一笔"(验幂等)。

- 鉴权、签名、时间戳挡在前面。

- 数据太干净:全是 happy path,真实世界全是缺字段、null、超长串、非法枚举。

- 非 HTTP 协议管不了:gRPC、Kafka、Redis。

- 把流量切到假服务本身麻烦:地址写死、要改 hosts/配置、还可能忘记切回。

三、传统 Mock 全貌:按你测的产品选工具

mock 工具本质按"在链路哪一层动手"区分:服务边界起假服务(本文主线,WireMock 是事实标准)、进程内打桩(单测)、浏览器拦截(前端联调):

几个不分产品形态、谁都可能用到的通用工具层:

- 进程内打桩(写后端单测、不起服务):Java Mockito 5(已原生支持 static/final)、字节 Mockey;Python responses/respx;JS Jest mock。阿里 TestableMock 已转维护期。有代码权限时它和 WireMock 配合:单测打桩覆盖分支,链路用 WireMock 验真实协作。

- 抓包 / 代理:Charles、Fiddler 适合手工排查;mitmproxy 能用 Python 脚本篡改流量、可进 CI。

- 接口管理平台:Apifox、Eolink、ApiPost(接口文档即 mock、原生 HTTPS);⚠️ 早年的 RAP/RAP2、YApi 已停更多年(YApi 还有历史漏洞),存量尽快迁走,新项目不要选。

- 流量录制回放:GoReplay、AREX、jvm-sandbox-repeater(后者社区版久未大更新,选型先看维护活跃度)。

- 消费者驱动契约:Pact + Pact Broker,多团队微服务要"改了会不会搞挂调用方"的发布门禁时才上,不替代链路测试。

- mock 大模型接口:LLM 的 OpenAI 风格接口本质也是 HTTPS,WireMock 照样能 stub 它的 /v1/chat/completions(文末专门展开)。

整体看:Web/H5/桌面最省心,微服务切流量最优雅,小程序卡域名白名单,App(尤其安卓)证书坎最高。 不分产品形态,WireMock 几乎都能覆盖。只有三种情况再换手段:依赖不是 HTTP(testcontainers)、纯前端独立联调(MSW/Apifox)、多团队微服务要发布门禁(Pact)。

四、从 0 到进 CI:一条 Mock 落地主线(手工做法)

落地工具选用WireMock——一个独立的假 HTTP 服务器,贯穿示例用一个出海广告投放系统:被测系统会主动调投放服务的 GET /api/campaign/{id}(拉广告计划),还会被媒体 POST /api/callback/conversion(转化回传)主动回调。

mock 从头到尾就这一件事:让你的程序把原本发往真实依赖(Google/Meta、支付、别的团队的服务)的请求改发到 WireMock,由它按预设规则返回正常、边界、故障、超时等各种响应——用来模拟那些不归你掌控、测试时却要召之即来挥之即去的下游。 WireMock 是个独立的假 HTTP 服务器,谁发 HTTP 请求来它都接(官方适配清单里 Android、Java/Spring、Python、Node、.NET、Go、Kubernetes、Testcontainers 全都有),不挑调用方,规则只按"方法+路径+报文"匹配。要不要用 WireMock、在哪一层用它?标准只有两条,都与产品形态无关:

- 依赖走不走 HTTP:走 HTTP,才用得上 WireMock 这类独立假服务器(单元测试里也可用 Mockito/Mockey、MSW/Nock 这类进程内拦截);不走 HTTP(数据库、Kafka、Redis),用 Testcontainers 起真实依赖,别硬 mock。

- 在哪一层测:单元测试进程内打桩、不起 WireMock;集成测试起 WireMock(主力);少量端到端打真实下游只跑 happy path——接 CI 那步会把这三层落成流水线的两个阶段。

这条主线固定是下面 10 步,任何接口的 mock 都绕不开:

配通环境 ①起 WireMock → ②切流量 → ③过 HTTPS(按需)

准备资产 ④取契约 → ⑤写第一条规则 → ⑥造三层数据

执行 ⑦造故障验重试 → ⑧跑用例·接 CI

反方向 ⑨第三方主动回调你

长期兜底 ⑩防漂移(契约锁 + 录制对拍,持续回头做)

4.1 第①步:先摆好拓扑,把 WireMock 跑起来

手机 App 跑在真机/模拟器,WireMock 跑在你 PC 上,两者连同一个 WiFi:

安卓 App ──HTTPS──▶ PC 上的 WireMock (192.168.x.x:8443)

└─ 按规则返回假的广告计划 / 媒体响应

需要 Java 11+(WireMock 3.x 的最低要求;只有老的 2.x 还支持 Java 8),下载 wiremock-standalone-3.x.jar(一个 fat jar,零依赖),同时开 HTTP 和 HTTPS:

java -jar wiremock-standalone-3.x.jar --port 8080 --https-port 8443

一个进程同时服务两个端口、同一批规则通用。它是标准 HTTP(S) 服务器,App、网页、微服务来请求都一样接,不挑调用方。假设 PC 内网 IP 是 192.168.1.10,先用手机浏览器访问 http://192.168.1.10:8080/__admin,能打开说明网络通了(打不开就查 PC 防火墙、手机和 PC 是否同网段)。规则文件放当前目录的 mappings/,改完自动热加载。

4.2 第②步:让请求发到 WireMock——App 三种切法 + 后端环境变量

WireMock 起来了,但 App 默认还把请求发到线上域名,得让它改道。这一步只解决"怎么改道",规则怎么写放下一步。App 按推荐度三选一:

- 首选:要一个 baseURL 可切换的测试包,直接把接口域名填成 https://192.168.1.10:8443。这是可测性需求,排期时就跟开发提,代理、hosts、证书的折腾能省一大半。

- 次选:手机 WiFi 挂代理(Charles/mitmproxy,再由它转投 WireMock),适合临时排查。

- root 设备改 hosts:安卓 /system/etc/hosts 加一行 192.168.1.10 api.你的下游.com(PC 端是 C:\Windows\System32\drivers\etc\hosts),让域名解析到 WireMock。

不想起服务、只想让 App 页面快速有数据?还有一条端内路子:测试包里加 OkHttp Interceptor,命中配置直接回包、不命中放行,请求不出端。代价是绕过了真实网络、验不了容错——本文主线用 WireMock,拦截器只在纯页面联调时备选。

同一回事,后端服务怎么切? 上面三法解决的是"手机里的 App 怎么改道";换成跑在服务器上的后端,它没有可点的 baseURL 设置页、也没法挂手机代理,切法是下游地址读环境变量/配置文件——和 App 法①本质一样,都是让地址可配置,只是配置的地方不同。就配两处:

# ① 被测服务侧 config.py:默认值给真实地址,不被覆盖就永远走真的,天然防"忘切回生产"

AD_API_BASE = os.getenv("AD_API_BASE", "https://api.partner.com")

# ② 起被测服务时覆盖成 WireMock,就这一行

# 本地手工联调:export AD_API_BASE=http://localhost:8080 后再启动服务

# CI / docker-compose:

environment: [AD_API_BASE=http://wiremock:8080]

手工联调和后面 CI 自动化注入的是同一个变量,只是注入方式不同。地址外置本来就是正经后端的配置规范(生产切预发也靠它),不是为测试做的改造;开发连这行都不肯加,才退回到代理/hosts-DNS/网关/注册中心那类网络层劫持(零改代码,代价是要过下面的证书关)。

4.3 第③步:过 HTTPS 这一关(按需,App 最疼的一道坎)

真实接口都是 HTTPS。WireMock 本身完全支持 HTTPS,难点从来不是工具,而是让 App 信任那张证书——命令行加个 -k 就能跳过校验,App 可不行。按你的条件选一档:

档① 测试包信任测试 CA(最规范,推荐)。 让 WireMock 用内部测试 CA 签发的证书启动:

java -jar wiremock-standalone-3.x.jar --https-port 8443 \

--https-keystore /certs/test-downstream.p12 \

--keystore-type PKCS12 --keystore-password 你的密码

安卓测试包配 networkSecurityConfig,只在 debug 构建信任这张 CA(res/xml/network_security_config.xml):

<network-security-config>

<debug-overrides>

<trust-anchors>

<certificates src="system" />

<certificates src="@raw/test_ca" />

</trust-anchors>

</debug-overrides>

</network-security-config>

档② DEBUG 包代码里临时信任所有证书(最快,风险也最高)。 拿不到规范测试包时用,核心示意:

// 仅测试包生效:信任任意证书 + 跳过主机名校验

private static final X509TrustManager TRUST_ALL = new X509TrustManager() {

public void checkClientTrusted(X509Certificate[] c, String a) {}

public void checkServerTrusted(X509Certificate[] c, String a) {}

public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }

};

static OkHttpClient.Builder bypassForMock(OkHttpClient.Builder b) throws Exception {

if (!BuildConfig.ENABLE_DEBUG_MOCK) return b; // 🔒安全闸:非 DEBUG 包直接返回,绝不装"信任所有证书"

SSLContext sc = SSLContext.getInstance("TLS");

sc.init(null, new TrustManager[]{TRUST_ALL}, new SecureRandom());

b.sslSocketFactory(sc.getSocketFactory(), TRUST_ALL)

.hostnameVerifier((host, sess) -> true);

return b;

}

🔴 必须用编译期 DEBUG 开关锁死在测试包,误带进线上等于关掉 HTTPS 全部保护。没源码、只能对现成包动手时才用 Frida / Xposed+JustTrustMe,且仅限自有 App、受控设备。

几个 App 特有的坑:

- 证书固定(Pinning):App 内置了服务器证书指纹,连测试 CA 也会被拒,测试包必须关掉 Pinning;

- QUIC/HTTP3 走 UDP 443 会绕开 TCP 代理,测试网络禁掉 UDP 443 逼它回落 TCP;

- iOS 不用写代码:装描述文件后还要去"设置→通用→关于本机→证书信任设置"手动开启,且受 ATS 限制默认禁明文。

档③ 下游是双向认证 mTLS——让 WireMock 反过来校验客户端证书。 支付、金融、内部系统常见:不止 App 要信服务器,服务器也要验 App 的客户端证书。让 WireMock 扮演这个下游时,挂上信任库、开启客户端证书校验:

java -jar wiremock-standalone-3.x.jar --https-port 8443 \

--https-keystore /certs/test-downstream.p12 --keystore-type PKCS12 \

--https-require-client-cert \

--https-truststore /certs/allowed-clients.p12 --truststore-type PKCS12

allowed-clients.p12 里放被测 App 的客户端证书;拿不出合法客户端证书的请求会在 TLS 握手阶段直接被拒——这本身也是一条要测的安全用例。

档④ 下游地址写死成 https、又装不了任何信任配置——用 Nginx 做 TLS 卸载。 老 App 既不能换包、也关不了证书校验,但下游域名能通过 hosts 指过来。让 Nginx 在 443 用合法 https 接住 App(证书由 Nginx 出、配进 App 信任库),解密后明文转给 WireMock 的 8080,把证书这道坎从 mock 工具挪给 Nginx——注意不是免证书,证书仍要让 App 信任:

server {

listen 443 ssl;

server_name api.partner.com; # App 要访问的下游域名

ssl_certificate /certs/partner.crt; # 配进 App 信任库

ssl_certificate_key /certs/partner.key;

# 下游若本就是 mTLS,可在此加 ssl_verify_client on; 由 Nginx 验客户端证书

location / {

proxy_pass http://127.0.0.1:8080; # 卸载 TLS 后明文转给 WireMock

proxy_set_header Host $host;

}

}

hosts 把 api.partner.com 指到这台 Nginx 即可。四档参数细节可对照 WireMock 官方 HTTPS 文档。

配通三步(起服务、切流量、过证书)做完,链路就通了。先用一个最简单的假响应验证改道成功——具体规则长什么样,正是下一步要写的。

4.4 第④步:取契约——动手写规则前,先搞清接口长什么样

很多人一上来就闷头写假 JSON,写完才发现字段是脑补的、金额单位搞反了,这正是痛点 3"字段靠脑补"。写规则前先拿到接口的真相(行话叫"契约"),来源按可靠度排:

- 有代码权限:DTO / VO、controller 的参数定义是最准的来源,字段名、类型、是否必填、校验注解都在上面;

- 有接口文档:OpenAPI/Swagger、Apifox 里的定义;

- 什么都没有:抓包(Charles/mitmproxy 导出 HAR)看真实请求响应,再找接口负责人口头确认。

抓包的 HAR 里混着埋点、心跳、鉴权刷新、静态资源,先把这些噪音剔掉,只留业务接口。对目标接口,手工也至少要在纸上/表格里列清这五项,一项都不能靠猜:

第⑤项最容易埋雷:枚举值域、金额单位(元/分)、时间字段的时区和窗口,必须找人闭环,宁可多问一句也别编。这张表就是后面写规则、造数据、做契约锁的共同依据——手工做法是自己填,下一章会看到 AI 能把 HAR/代码一键清洗成这张表。

4.5 第⑤步:写第一条规则(mapping)

链路通了、契约会了,现在写第一条真正的规则,验证端到端闭环。在 mappings/campaign.json 放:

{

"request": { "method": "GET", "urlPathPattern": "/api/campaign/[0-9]+" },

"response": {

"status": 200,

"headers": { "Content-Type": "application/json" },

"jsonBody": { "campaign_id": 1001, "name": "测试广告计划", "budget": 5000, "status": "ACTIVE" }

}

}

打开 App 的计划详情页(或用 curl/Postman 打被测系统),看到的正是这条假数据——第一个闭环通了。 规则里每个字段都对得上上一步那张契约表:budget 是整数分、status 取确认过的枚举,没有一个字段是拍脑袋的。

规则有两种配法,按用途选:

- 放 mappings/ 目录:进 Git、可评审、改完热加载,适合稳定的、要长期保留的规则;

- 运行时 Admin API 动态建:在用例里临时灌、用完即弃,适合每个用例各自不同的故障/数据:

curl -X POST http://192.168.1.10:8080/__admin/mappings \

-H 'Content-Type: application/json' -d @campaign.json

多条件的规则还能按 header、query、body 匹配,也可以给不同入参返回不同结果,这些按需查 WireMock stubbing 文档。

4.6 第⑥步:一次造三层数据——正常 / 边界 / 脏

只造一条 happy path 的假数据,等于只验证了"系统在一切顺利时不崩"。真实缺陷和资损几乎都藏在边界和脏数据里(痛点 6)。手工做法:围绕上一步的契约,把同一接口的数据按等价类分成三组,逐条造、逐条让被测系统过一遍:

C 层是重点中的重点:一条脏数据被当成合法数据收下,就是一个真 Bug(浮点金额、非法枚举、私加字段是最高发的三类,布尔是 int 子类、金额单位这类隐蔽项要专门造)。

手工造几条可以,但成批造很慢、还容易造得雷同。两个落地办法:

- 数据少:直接写在 mapping 的 jsonBody,或一个场景一个 mapping 文件;

- 数据成批:放 __files/ 目录存 JSON、mapping 里用 bodyFileName 引用;或写个脚本遍历数据文件,每条走 Admin API 动态灌。

这一步是手工和 AI 差距最大的环节:人编 5 条带业务关联的数据就累,AI 配 Faker 一次能给几百条还自带跨字段关联——下一章给现成提示词。但手工也必须先理解这三层的判法,否则 AI 造出来你也判断不了对错。

4.7 第⑦步:造故障和异常,验证重试与容错

光有正常/边界数据还不够,WireMock 最值钱的是不写业务代码就能模拟下游抽风,全在 response 里配:限流/服务错误 "status": 429 或 500;超时 "fixedDelayMilliseconds": 5000;断连/畸形响应 "fault": "CONNECTION_RESET_BY_PEER" / "MALFORMED_RESPONSE_CHUNK"。

重点看有状态的"前两次失败、第三次成功"——用来验证被测系统的重试逻辑,用 Scenarios(状态机)。把三条规则放进 mappings/ 下同一个文件:

{

"mappings": [

{ "scenarioName": "retry-then-success", "requiredScenarioState": "Started",

"newScenarioState": "r1",

"request": { "method": "GET", "urlPath": "/api/campaign/1001" },

"response": { "status": 500 } },

{ "scenarioName": "retry-then-success", "requiredScenarioState": "r1",

"newScenarioState": "r2",

"request": { "method": "GET", "urlPath": "/api/campaign/1001" },

"response": { "status": 500 } },

{ "scenarioName": "retry-then-success", "requiredScenarioState": "r2",

"request": { "method": "GET", "urlPath": "/api/campaign/1001" },

"response": { "status": 200,

"jsonBody": { "campaign_id": 1001, "name": "测试广告计划", "status": "ACTIVE" } } }

]

}

一组有状态规则放进 mappings/ 下同一个文件时,外层必须包一层 { "mappings": [ ... ] },不能直接丢一个裸数组(裸数组只在批量导入接口 POST /__admin/mappings/import 里才合法;文件里这么写不会被加载)。注意:凡是从 mappings/ 文件加载的规则都不适合在脚本里动态增删(文件规则归文件、热加载即可);要在用例里动态建、用完即弃,就走 Admin API——单条 POST /__admin/mappings,或一次性 POST /__admin/mappings/import 传 { "mappings": [ ... ] }。

怎么证明系统真的重试了?问 WireMock 要"被调次数":

curl -X POST http://192.168.1.10:8080/__admin/requests/count \

-H 'Content-Type: application/json' \

-d '{"method":"GET","urlPath":"/api/campaign/1001"}'

返回 {"count":3} 就是重试了 3 次的铁证;再配一个"恒 500",验证重试到上限后系统给出失败提示、次数停在上限。重试、熔断是被测系统自己的行为,WireMock 只按剧本返回,你只负责数次数,别在 mock 里替它实现重试。

4.8 第⑧步:跑用例、接进自动化和 CI

这一步把它沉淀成自动化回归并进 CI。技术栈用 Python + pytest + requests + docker compose,思路对任何语言都通用。

这一步以后端服务形态演示。 理由很实际:CI 是无人值守、随时要从干净环境从零拉起的,后端一条 docker compose up 就能把 WireMock 和被测服务一起起好、下游地址用环境变量注入、同一份代码两个阶段各部署一次——最轻、人人可复现;App 进 CI 还得准备模拟器/真机、装测试包、跑 Appium,重且不稳,不适合当最小示范。但 WireMock 侧的用法和被测形态无关:被测的是 App 时,把下面"pytest 直调被测服务"换成 Appium/Espresso 驱动那个指向 WireMock 的测试包即可,规则、Admin API 控制通道、数被调次数一个字都不用改。

下面按 ① 搭工程 → ② 放被测服务 → ③ 写夹具 → ④ 写用例 → ⑤ 写 compose 切流量 → ⑥ 本地两阶段跑 → ⑦ 挂 CI 的顺序走,每一步都有完整文件,照着建目录、贴代码就能跑通。

① 搭出工程目录。 最小可跑工程长这样,先把空目录和文件建出来:

mock-demo/

├── docker-compose.yml # 编排 WireMock + 被测服务

├── svc/ # 被测服务示例(你真实项目里换成自己的服务)

│ ├── app.py # 读 AD_API_BASE 转发下游、带重试

│ └── Dockerfile

├── wiremock/mappings/ # 稳定规则可挂卷进 Git

└── tests/

├── conftest.py # 连 WireMock/被测系统的公共夹具(两条通道 + 复位)

├── test_normal.py # 正常用例(无 marker,阶段①跑)

├── test_fault.py # 故障用例(打 mock 标)

└── test_smoke_real.py # 真下游冒烟(打 real 标 + 默认 skip)

② 放进被测服务。 真实项目里这就是你要测的后端,这里用标准库写个零依赖最小替身,关键只看它两点:从环境变量读下游地址(第②步主线说的"地址外置")、下游 5xx 最多重试 3 次(正好被后面的故障用例验证)。新建 svc/app.py:

# svc/app.py —— 一个会转发下游、且带重试的最小被测服务

import json, os, time, urllib.request, urllib.error

from http.server import BaseHTTPRequestHandler, HTTPServer

AD_API_BASE = os.getenv("AD_API_BASE", "https://api.partner.com") # 不被覆盖就走真的

MAX_RETRY = 3

def call_downstream(campaign_id):

url = f"{AD_API_BASE}/api/campaign/{campaign_id}"

for _ in range(MAX_RETRY):

try:

with urllib.request.urlopen(url, timeout=5) as r:

return r.status, r.read().decode()

except urllib.error.HTTPError as e: # 下游返回 500 会进这里

if 500 <= e.code < 600:

time.sleep(0.1) # 真实项目是退避策略,这里加速测试

continue

return e.code, e.read().decode()

except Exception:

time.sleep(0.1)

return 503, json.dumps({"error": "downstream unavailable after retries"})

class Handler(BaseHTTPRequestHandler):

def do_GET(self):

if self.path.startswith("/campaigns/"):

cid = self.path.rsplit("/", 1)[-1]

status, body = call_downstream(cid)

self.send_response(status)

self.send_header("Content-Type", "application/json")

self.end_headers

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 591 款 AI 工具分类导航 · 每日 AI 热门项目与动态