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

0 阅读28分钟

执行自动化 ·(六)

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

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

#问题真实场景
1依赖不可控三方测试环境时好时坏,返回 429/500/超时,你以为测出 Bug,其实是对方挂了
2场景造不出想测"同一笔转化被回调两次""签名错误""金额为 0/超大",对方不可能配合专门返回
3有副作用、烧钱真调支付=真扣款,真发短信=真花钱,真发券=真核销
4下游没好,被阻塞你的功能做完了,依赖的服务还没上线,干等
5结果不可重复依赖返回每次都变(库存、汇率、时间窗),用例时过时不过
6数据准备难要造"某用户恰好 3 笔订单、一笔跨了 7 天归因窗口",真实系统里极其痛苦

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


二、Mock 的八个真实痛点

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

  1. HTTPS 拦不下来:不是工具不支持 HTTPS,而是证书信任链卡人——自签证书客户端默认不信(PKIX path building failed)、mTLS 要客户端证书、App 证书固定(Pinning)+ Android 7+ 不信用户证书连抓包都抓不到、QUIC/HTTP3 绕代理。
  2. Mock 被动不进化(最危险):后端把 status 改成 state、金额单位从元改分,你的假服务还按旧规则返回,测试一片绿,切真环境就崩。
  3. 搭起来慢、维护更慢:一条条手写匹配规则和返回体,接口一多就是体力活。
  4. 有状态、有顺序的场景难配:"前两次 500 第三次成功"(验重试)、"回调发两次账只记一笔"(验幂等)。
  5. 鉴权、签名、时间戳挡在前面。
  6. 数据太干净:全是 happy path,真实世界全是缺字段、null、超长串、非法枚举。
  7. 非 HTTP 协议管不了:gRPC、Kafka、Redis。
  8. 把流量切到假服务本身麻烦:地址写死、要改 hosts/配置、还可能忘记切回。

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

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

产品形态用什么 mock(前端联调 / 接口·链路集成 / 单测)怎么把流量切到假服务证书 / 拦截这道坎
Web(PC 浏览器)联调 MSW(拦 fetch/XHR)/集成 WireMock/单测 Vitest/Jest mock改 baseURL;或 devServer proxy、Charles Map Remote低:信任测试证书即可;注意假响应带 CORS 头、开了 HSTS 的域名强制 HTTPS
H5(手机浏览器 / App 内 WebView)MSW / WireMock / —改 baseURL;手机或 WebView 挂代理(Charles/mitmproxy)转投低-中:证书跟手机系统走;iOS 有 ATS;WebView 用 vConsole / Safari inspect 调试
小程序(微信/支付宝等)开发者工具内置 mock 或 Apifox /真机链路 WireMock / —工具勾"不校验合法域名"可直连内网;真机/体验版必须 HTTPS 且域名在后台 request 白名单,测试域名经 Nginx 反代到 WireMock中:域名要备案 + 进白名单,经 Nginx 做 TLS 卸载
Android AppWireMock(验链路)/OkHttp Interceptor(纯造数据)/单测 Mockito①首选开发给 baseURL 可切换的测试包 ②WiFi 挂代理 ③root 改 /system/etc/hosts最高:Android 7+ 默认不信用户证书、Pinning、QUIC 绕代理,落地见 4.2~4.3
iOS AppWireMock/URLSession 拦截/单测 XCTest mock测试包改配置;或 WiFi 挂代理高:装根证书描述文件并在"关于本机→证书信任"手动启用;ATS 默认禁明文;Pinning 测试包关
微服务 / 后端服务—/WireMock、MockServer;非 HTTP 用 testcontainers;跨团队契约用 Pact/单测 Mockito配置中心改下游地址;或 Nacos/Eureka 注册成指向 WireMock 的实例;地址走环境变量低:内网常走明文;Istio 等 mesh 下有 mTLS,用 4.3③;难点在有状态编排与契约一致性
桌面客户端(Electron 等)MSW/WireMock(内核是 Chromium,同 Web)/—改配置或系统代理低,同 Web

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

  • 进程内打桩(写后端单测、不起服务):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 按推荐度三选一:

  1. 首选:要一个 baseURL 可切换的测试包,直接把接口域名填成 https://192.168.1.10:8443。这是可测性需求,排期时就跟开发提,代理、hosts、证书的折腾能省一大半。
  2. 次选:手机 WiFi 挂代理(Charles/mitmproxy,再由它转投 WireMock),适合临时排查。
  3. 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"字段靠脑补"。写规则前先拿到接口的真相(行话叫"契约"),来源按可靠度排:

  1. 有代码权限:DTO / VO、controller 的参数定义是最准的来源,字段名、类型、是否必填、校验注解都在上面;
  2. 有接口文档:OpenAPI/Swagger、Apifox 里的定义;
  3. 什么都没有:抓包(Charles/mitmproxy 导出 HAR)看真实请求响应,再找接口负责人口头确认。

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

要搞清的项例子(GET /api/campaign/{id})
① method + pathGET,/api/campaign/1001
② 请求必填字段、类型、约束path 上的 id 为整数;如有 query,币种枚举
③ 响应字段、类型、示例campaign_id:int、name:string、budget:int(分)、status:枚举
④ 鉴权 / 签名方式Bearer Token / HMAC 签名头
⑤ 不确定、要找人确认的点status 的全部枚举值、金额到底是元还是分、时间窗边界

第⑤项最容易埋雷:枚举值域、金额单位(元/分)、时间字段的时区和窗口,必须找人闭环,宁可多问一句也别编。这张表就是后面写规则、造数据、做契约锁的共同依据——手工做法是自己填,下一章会看到 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)。手工做法:围绕上一步的契约,把同一接口的数据按等价类分成三组,逐条造、逐条让被测系统过一遍:

层造什么(以广告计划/金额为例)期望
A 正常合法字段、跨字段合理(budget=5000 分、时间窗正常、枚举合法)走通,返回 200
B 边界金额 0 / 1 / 极大值、时间窗恰为第 7 天 0 点与 23:59:59、超长串、空串按需求正确处理或明确报错,不能崩、不能脏写
C 脏缺必填、类型错(金额给字符串)、非法枚举、契约外多字段(如私加 coupon_name)、null、结构错位必须全部被拒绝,且拒绝原因正确

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()
            self.wfile.write(body.encode())
        else:
            self.send_response(404); self.end_headers()
    def log_message(self, *a):  # 静默默认日志
        pass

if __name__ == "__main__":
    HTTPServer(("0.0.0.0", int(os.getenv("PORT", "8000"))), Handler).serve_forever()
# svc/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .
ENV PORT=8000
CMD ["python", "app.py"]

注意被测服务对外暴露的是 /campaigns/{id}(测试打这个),它转发给下游的才是 /api/campaign/{id}(WireMock 的 stub 匹配这个)——两个路径别搞混。

③ 写公共夹具 tests/conftest.py。 它把两条通道封装好:数据通道只打被测服务,控制通道走 WireMock Admin API。关键是每条用例前后都复位(清规则、清请求日志、场景归位),否则被调次数会把上一条用例的调用累计进去:

# tests/conftest.py
import os, requests, pytest

# sut = System Under Test(被测系统),测试代码里的通用叫法;常量加 _BASE 避免和同名夹具撞名
SUT_BASE = os.getenv("SUT_URL", "http://localhost:8000")      # 数据通道(只打它)
WM_BASE  = os.getenv("WIREMOCK_URL", "http://localhost:8080") # 控制通道

@pytest.fixture
def wm():
    class _WireMockAdmin:
        def add_stub(self, stub):
            r = requests.post(f"{WM_BASE}/__admin/mappings", json=stub, timeout=5)
            r.raise_for_status()
        def add_scenario(self, stubs):                        # 多条:批量导入
            r = requests.post(f"{WM_BASE}/__admin/mappings/import",
                              json={"mappings": stubs}, timeout=5)
            r.raise_for_status()
        def count(self, method, path):
            r = requests.post(f"{WM_BASE}/__admin/requests/count",
                              json={"method": method, "urlPathPattern": path}, timeout=5)
            return r.json()["count"]
        def reset(self):                                      # 每条用例前后复位,防串味
            requests.post(f"{WM_BASE}/__admin/scenarios/reset", timeout=5)
            requests.delete(f"{WM_BASE}/__admin/mappings", timeout=5)
            requests.delete(f"{WM_BASE}/__admin/requests", timeout=5)  # 清请求日志,否则 count 累计
    w = _WireMockAdmin(); w.reset(); yield w; w.reset()

@pytest.fixture
def sut():
    class _SutClient:
        def get_campaign(self, cid):
            return requests.get(f"{SUT_BASE}/campaigns/{cid}", timeout=10)
    return _SutClient()

⚠️ 常量别和夹具起同一个裸名(比如夹具叫 sut、模块常量也叫 SUT 又在类体内引用)——Python 类作用域会把它解析到类对象本身,URL 变成 <class ...>/__admin/...,用例全报错。这里常量用 SUT_BASE、夹具用 sut,既保留业内通用叫法又不撞名。

④ 写三类用例。 正常用例不打标、故障打 mock 标、真下游冒烟打 real 标;两阶段都用正向选择,别用 -m "not mock"(它会把没打标的正常用例也带进真下游阶段)。先在 pytest.ini 注册 marker,再逐个建文件:

# pytest.ini
[pytest]
markers =
    mock: 对 WireMock 注入故障的用例
    real: 打真实下游的预发冒烟
# tests/test_normal.py —— 正常 happy path,不打标,只在阶段①跑
def test_campaign_happy_path(wm, sut):
    wm.add_stub({
        "request": {"method": "GET", "urlPath": "/api/campaign/1001"},
        "response": {"status": 200,
                     "jsonBody": {"campaign_id": 1001, "name": "测试广告计划", "status": "ACTIVE"}}})
    r = sut.get_campaign(1001)
    assert r.status_code == 200
    assert wm.count("GET", "/api/campaign/1001") == 1
# tests/test_fault.py —— 整个文件打 mock 标;断言就是反复强调的三样:HTTP码 / 数据 / 被调次数
import pytest
pytestmark = pytest.mark.mock

def test_retry_then_success(wm, sut):
    wm.add_scenario([
        {"scenarioName": "retry", "requiredScenarioState": "Started", "newScenarioState": "r1",
         "request": {"method": "GET", "urlPath": "/api/campaign/1001"}, "response": {"status": 500}},
        {"scenarioName": "retry", "requiredScenarioState": "r1", "newScenarioState": "r2",
         "request": {"method": "GET", "urlPath": "/api/campaign/1001"}, "response": {"status": 500}},
        {"scenarioName": "retry", "requiredScenarioState": "r2",
         "request": {"method": "GET", "urlPath": "/api/campaign/1001"},
         "response": {"status": 200, "jsonBody":
                      {"campaign_id": 1001, "name": "测试广告计划", "status": "ACTIVE"}}},
    ])
    r = sut.get_campaign(1001)
    assert r.status_code == 200                          # ① HTTP 码:重试后成功
    assert r.json()["name"] == "测试广告计划"            # ② 数据:拿到第三次的假数据
    assert wm.count("GET", "/api/campaign/1001") == 3   # ③ 被调次数:正好 3 次
# tests/test_smoke_real.py —— 阶段②真下游冒烟,默认 skip 防误烧钱
import os, pytest
pytestmark = pytest.mark.real
@pytest.mark.skipif(os.getenv("ALLOW_REAL_CALL") != "1",
                    reason="默认不打真下游,预发设 ALLOW_REAL_CALL=1 再跑")
def test_real_campaign_happy_path(sut):
    r = sut.get_campaign(1001)
    assert r.status_code == 200 and "campaign_id" in r.json()

⑤ 写 docker-compose.yml,切流量就这一下。 同时编排 WireMock 和被测服务,给被测服务注入下游地址 AD_API_BASE——这就是主线第②步在容器环境的落点:

services:
  wiremock:
    image: wiremock/wiremock:3.9.2
    ports: ["8080:8080"]
  svc:
    build: ./svc
    ports: ["8000:8000"]
    environment:
      AD_API_BASE: http://wiremock:8080     # ◀◀ 切流量的唯一动作(容器内用服务名互访)
    depends_on: [wiremock]

注意两个地址别搞反:容器里的被测服务走 docker 网络,用服务名 http://wiremock:8080 访问假服务(所以 compose 注入这个);跑在宿主机的 pytest 走端口映射,访问 http://localhost:8080 和 http://localhost:8000(即 conftest 里的默认值)。

⑥ 本地按两个阶段各跑一遍,确认全绿:

# 阶段①:起 mock 环境,跑全部用例(正常+故障;真下游冒烟默认 skip)
docker compose up -d --build
pip install pytest requests && pytest tests/ -v

# 阶段②:连预发真下游,只跑打 real 标的冒烟
SUT_URL=http://预发地址 ALLOW_REAL_CALL=1 pytest tests/ -m real -v

⑦ 挂进 CI。 本地两阶段跑通后,把同样两条命令放进流水线即可。GitHub Actions 为例,新建 .github/workflows/mock-test.yml(Jenkins / GitLab CI 把两个 job 换成对应的两个 stage,命令完全一样):

# .github/workflows/mock-test.yml
name: mock-test
on:
  pull_request:            # 每个 PR 跑阶段①,挡住回归
  push:
    branches: [main]       # 合进 main 再跑阶段②真下游冒烟

jobs:
  integration-mock:                          # 阶段①:起 mock,跑全部正常+故障
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install pytest requests
      - run: docker compose up -d --build
      - name: wait for services              # 等 WireMock 健康、被测服务端口起来
        run: |
          for i in $(seq 1 30); do
            curl -sf http://localhost:8080/__admin/health >/dev/null &&
            curl -s -o /dev/null http://localhost:8000/campaigns/0 && break
            # 第二个不加 -f:该路径无 stub 会回 404,只要有 HTTP 响应就证明被测端口已起
            sleep 2
          done
      - run: pytest tests/ -v                # 冒烟默认 skip;也可 -m mock 只跑故障

  staging-smoke:                             # 阶段②:真下游冒烟,只在 main 上
    needs: integration-mock
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install pytest requests
      - env:
          SUT_URL: ${{ secrets.STAGING_SUT_URL }}   # 在仓库 Settings → Secrets 配预发地址
          ALLOW_REAL_CALL: "1"
        run: pytest tests/ -m real -v

阶段②要在仓库 Settings → Secrets and variables → Actions 里配一个 STAGING_SUT_URL 指向预发环境;没配之前冒烟保持 skip,不会误烧钱。

落地时记住三条:

  • 两条通道别混:测试发业务请求只打被测服务,绝不直接打 WireMock;灌规则、复位、数次数全走 Admin API。下游地址由 compose/CI 在起服务时注入,测试脚本里没有这个变量。
  • 定位:更上面一层单元测试用 Mockito/Mockey 进程内打桩、不起 WireMock;阶段①是集成测试主力;阶段②就是少量 E2E 的自动化形态。

上面这套夹具、用例、compose 已用 WireMock 3.9.2 + pytest 实跑通过(正常调 1 次、前两次 500 第三次成功数到 3 次、恒 500 耗尽降级各一条:3 passed、1 skipped,-m real 精确只选中冒烟)。

4.9 第⑨步:反方向——第三方主动回调你的系统,怎么测

前面都是"我们的 App/后端主动调第三方"(要把下游换成 WireMock)。但广告系统还有另一个方向,方向反了做法完全不同,也是很多人绕晕的地方:

媒体把转化事件主动 POST 回你的系统(用户在 Google 点了广告、后续付费,Google 服务器回调你的转化回传接口通知你记账)。

这种"别人调你"的接口,根本不需要 WireMock——本来就是外部发请求给你,测试时你自己站在外部、扮演 Google/Meta 往你的接口发 HTTP 请求就行,改地址、证书一概不涉及,是最该先做、零开发配合的一类测试:

你扮演媒体发什么验证什么
一笔签名正确的正常转化回调返回 200,后台账单多一笔
同一个 event_id 立刻发两次幂等:账单始终只有一笔,第二次返回重复标识
签名错误 / 过期返回 401,账单不增加
畸形 JSON、缺字段、金额为负返回 400,不记账
先下单回调、退款回调乱序到达最终状态正确,不重复扣款

这一层你能断言的永远是三样:HTTP 返回码、查后台接口看到的数据副作用(账记了几笔)、以及——只有"你调别人"时才有——WireMock 记录的被调次数。 在自动化里它就是一个普通的 pytest:直接 requests.post(SUT_BASE + "/api/callback/conversion", ...),然后查你系统的账单接口核对笔数,不涉及 WireMock。

4.10 第⑩步:防漂移——契约锁 + 录制对拍(最容易被跳过、也最值钱)

mock 最大的长期风险是:真实接口悄悄改了,你的假服务还按旧契约返回,于是测试一片绿、切真环境就崩。 这一步不是一次性动作,是要长期、反复、卡进 CI 的兜底:

  1. 契约锁:把第④步那张契约表落成一个确定性的校验器(JSON Schema 或一段校验代码,不能靠人眼、更不能交给 AI 判断),同一把尺子两头量——既校验你造的假数据/规则没超契约,也在 CI 里校验真实响应、抓包流量还符不符合契约。一次性收集全部违规(缺字段、类型错、非法枚举、契约外字段),典型高发的是浮点金额、非法枚举、私加字段(如 coupon_name),bool 是 int 子类、金额单位这类隐蔽项要在锁里显式拦。
  2. 录制对拍:用 WireMock 的录制能力定期跑一次真实链路(--record-mappings --proxy-all https://真实下游),把真实响应录下来,和契约、已有 mapping 做 diff——字段一改名、一换类型立刻暴露,对拍不过就卡住合并。
CI 闭环:取契约 → 起 WireMock(HTTPS+测试CA) → 灌规则/数据/故障(过契约锁)
      → 部署被测系统(下游指向 WireMock) → 跑用例 → 录制/对拍真实响应防漂移 → 数次数/查账 → 销毁

五、AI 怎么在这条主线上提效

第四章主线:①起服务 → ②切流量 → ③过HTTPS → ④取契约 → ⑤写规则 → ⑥造三层数据
                          → ⑦造故障 → ⑧跑用例·接CI → ⑨回调 → ⑩防漂移
AI 能做:      🟡帮配/查错    🟡      🟡       🤖清洗    🤖生成    🤖批量造
                          🤖翻状态机  🤖生成脚本/诊断   —       🔒锁必须代码
🤖 AI 可大幅加速 🔒 必须确定性代码/人把关 🟡 AI 只帮配置和解读报错,不碰业务代码
  • AI 主要加速"产出"环节(④取契约、⑤写规则、⑥造数据、⑦造故障),以及帮你生成脚手架、解读报错;
  • 两处判定权必须留给代码和人——⑧用例的最终断言、⑩契约校验/对拍;
  • 侵入性:10 步里 9 步完全在被测系统外部做、零侵入;唯一可能碰到工程的是②切流量——首选只是让开发把下游地址留成一个可配置项(1 行,属"配置与代码分离"的通用规范,不动业务逻辑、release 不变),连这行都不配合时退到代理/hosts 网络劫持,也能零改代码(代价是过 HTTPS 证书关)。

这张表逐个环节说清 AI 具体做什么、人把什么关:

主线环节AI 能做什么必须由代码 / 人把关
①②③ 配通生成切流量配置、networkSecurityConfig/TrustManager/Nginx 片段;解读 PKIX、证书不匹配等报错、给排查方向证书私钥管理、release 包安全开关由人确认
④ 取契约把 HAR/OpenAPI/DTO 清洗成契约表、列待确认项枚举值域、金额单位等待确认项必须找人闭环
⑤ 写规则生成 mapping JSON、400 变体灌之前过第⑩步契约锁
⑥ 造数据Faker 批量造正常/边界/脏三层、标注每条脏数据预期被拒原因C 组是否被正确拒绝,由断言判定
⑦ 造故障业务话翻成 Scenarios、连带产出断言清单重试/幂等是被测系统的逻辑,AI 不替它实现
⑧ 跑用例·接 CIMCP 执行期临时改剧本、查被调次数、诊断为何没命中;生成 pytest+Admin API+复位的驱动脚本、compose 与流水线 setup/teardown对错判定必须写死在断言里、门禁与合并卡点由团队定
⑩ 防漂移解读录制 diff、提示哪里可能脱节校验/对拍本身必须确定性代码

5.1 先把 AI 接到 WireMock 上:两种方式

2025 年起主流 mock 工具已把 AI 做成原生能力:MockServer 内置 MCP 能用自然语言建期望、查请求、诊断不匹配,Microcks 有 AI Copilot 从 OpenAPI 生成动态响应,WireMock 官方也提供两条路。

方式 A:让 AI 写 mapping,你手动灌进去(零成本,内网也能用)

你把接口长什么样告诉AI,它吐出一段 WireMock 看得懂的 JSON;把这段 JSON 交给 WireMock:

你 ──贴接口契约──▶ AI 对话框(豆包 / DeepSeek / Kimi / ChatGPT 任意一个)
                         │  AI 吐出一段 mapping JSON
                         ▼
你复制 JSON ──▶ ① 存成 mappings/xxx.json(WireMock 自动热加载)
             或 ② curl POST 到 http://localhost:8080/__admin/mappings
                         │
                         ▼
                    WireMock 生效,App/接口请求拿到假数据

为什么随便一个 AI 都会写?WireMock 是开源老牌,mapping 格式完全公开,主流大模型的语料里有大量文档和示例,不装插件、不用注册,它天然认得 3.x 格式:

  1. 按第四章把 WireMock 起好(HTTP/HTTPS 两个端口都开着);
  2. 打开任意 AI 对话框,用大白话把接口讲清楚,比如直接说:"我用 WireMock 3.x,帮我写 stub:POST /api/callback/conversion,请求体含 click_id、event_id、event_type、amount(整数,单位分);正常返回 200 和 {"code":0},再给一个缺 event_id 时返回 400 的变体。只给 JSON。"
  3. AI 吐出两段 JSON,你把正常那段存成 mappings/conversion.json——文件一存,WireMock 控制台立刻打印"已加载"日志;
  4. 不想存文件、要在自动化脚本里动态建,就灌进 Admin API:curl -X POST http://localhost:8080/__admin/mappings -H 'Content-Type: application/json' -d @conversion.json;
  5. 立刻验证:curl -X POST http://localhost:8080/api/callback/conversion -H 'Content-Type: application/json' -d '{"click_id":"c1","event_id":"e1","event_type":"purchase","amount":9900}',拿到 AI 编的 {"code":0}——闭环完成。

两个注意:提示词里写明 WireMock 大版本(2.x 和 3.x 的模板语法有差异);AI 给的 JSON 先过一遍契约校验再灌,防它脑补字段。至于"让 AI 自己扫代码库、把所有出站调用一次性找出来",需要 Claude Code、Cursor 这类能读取本地工程文件的 AI(网页版对话框读不到你的代码),官方文档给的经典提示词是:

Scan this repository and generate WireMock 3.x JSON stub mappings for all
outbound HTTP calls. Use request matching on method and URL. Where response
bodies can be inferred from usage, include them.

方式 B:官方 MCP + Agent Skills(AI 直接操作 WireMock,进阶/团队)

WireMock Cloud 提供原生 MCP server:AI 不再只"生成文本让你复制",而是直接调用工具——建 mock、灌 stub、录流量、查请求日志。按下面四步配(官方安装文档):

① 装 CLI 并登录(需要 Node 18+;Cloud 有免费账号):

npm i -g @wiremock/cli
wiremock login

② 把 MCP 接进你的 AI 工具,三选一:

# Claude Code:stdio 方式(推荐,走刚装的 CLI)
claude mcp add wiremock -- wiremock mcp
# 或不装 CLI,直接连远程 MCP:
claude mcp add --transport http wiremock https://mcp.wiremock.cloud/mcp

Cursor 编辑项目根目录 .cursor/mcp.json:

{
  "mcpServers": {
    "WireMock": { "url": "https://mcp.wiremock.cloud/mcp" }
  }
}

VS Code + Copilot 编辑 settings.json:

{
  "mcp": {
    "servers": {
      "WireMock": { "type": "stdio", "command": "wiremock", "args": ["mcp"] }
    }
  }
}

重启 CLI 或编辑器,问一句 "Am I logged into WireMock Cloud?" 验证已连通。

③(可选)装官方 Agent Skills——打包好的多步工作流,在 Claude Code 会话里:

/plugin marketplace add wiremock-inc/skills
/plugin install wiremock-cloud@wiremock-inc-skills

接上之后,第四章那些手把手敲的手工动作全部变成一句话——这正是方式 B 区别于方式 A 的价值:AI 不只生成文本,而是直接调用工具把 mock 建好。几个官方 Agent Skills 命令最能说明问题:

  • /build-api-simulation:丢给它 OpenAPI 或一句描述(如"mock 支付接口,带超时和间歇性 500"),它建 mock API、生成 stub、直接给你可用 URL;
  • /create-stubs 自动生成并导入规则;说"前两次 500、第三次成功",它自动配出 Scenarios 状态机;
  • /convert-to-data-driven 把 stub 接到 CSV 或数据库批量造数;/validate-and-fix-stubs 按 OpenAPI 校验并自动修复 stub;
  • 运行中直接问"点击接口被调了几次",它查 request journal 返回。

官网演示的原话是输入 "mock our payments API, with timeouts and a flaky sandbox",AI 自动建 mock、加超时和故障场景,几秒给出一个可访问的模拟地址。

内网和 CI 怎么办: WireMock Runner 让 Cloud 管理的 mock 在本地或防火墙内跑——wiremock run 读项目里的 .wiremock 目录一键起多个 mock(各占一个端口);CI 里 wiremock run & 后台起服务再跑测试。完全离线、连 Cloud 都不能用的,就走方式 A + 开源 jar,用 docker-compose 把 WireMock 和被测服务一起编排起来(注入下游地址的写法第②步已经给过)。

怎么选:个人学习、内网敏感 → 方式 A,下面提示词直接可用;团队协作、要自然语言驱动和漂移治理 → 方式 B。无论哪种,AI 生成的 stub 都要进 Git 过评审、过第⑩步那道契约锁——"AI 直接操作 mock"省的是打字,不是审查。

5.2 AI 驱动的五步:主攻④⑤⑥⑦⑩(提示词直接抄)

通道通了之后,AI提效主要是"准备资产"这一段:④取契约、⑤写规则、⑥造数据、⑦造故障,外加⑩那道锁。下面五步就是这五个环节的 AI 做法。

步骤对应主线AI 的产物治哪个痛点
第 1 步 取契约④字段契约表 + 待确认清单搭得慢、字段靠脑补
第 2 步 写规则⑤stub 规则(含 400 变体)手写规则体力活
第 3 步 造数据⑥三层 JSONL + Faker 脚本数据太干净
第 4 步 编故障⑦(顺带⑨)故障 stub + 断言清单有状态场景难配
第 5 步 防漂移⑩校验器、对拍任务mock 被动不进化(最危险)
第1步取契约 ─▶ 第2步写规则 ─▶ 第3步造数据 ─▶ 第4步编故障 ─▶ [第⑧步跑用例] ─▶ 第5步契约锁+对拍
  AI           AI            AI            AI          系统执行         确定性代码,不交 AI

第 1 步:把接口真相嚼成契约(主线④)

最省事的来源是抓包(HAR)或 OpenAPI/接口文档;有代码权限时,DTO、controller 的参数定义更准,方式 B 下甚至可以直接让 AI 扫代码库(用的就是方式 A 给过的那句官方提示词)。拿不到代码就找接口负责人确认。HAR 里混着埋点、心跳、鉴权刷新,让 AI 先清洗。提示词 P1(可直接复制):

你是接口测试专家。下面是接口的 HAR 抓包 / OpenAPI 文档 / 代码定义(任一或组合)。
只提取业务相关接口,忽略埋点/心跳/静态资源/鉴权刷新。对每个接口输出:
① method+path ② 请求必填字段、类型、约束 ③ 响应字段、类型、示例
④ 鉴权/签名方式 ⑤ 你不确定、需找人确认的点(尤其枚举值域、金额单位元/分、时间窗)。
输出 Markdown 表,不要编造材料里没有的字段。
【贴材料】

产出是一张契约表 + 一份待确认清单。每个字段都要能在材料里找到出处,"待确认"项必须闭环。

第 2 步:让 AI 直接生成 WireMock 规则(主线⑤)

提示词 P2,产出就是手工写过的那种 mapping JSON,把"别脑补"写死:

基于下面的接口契约生成 WireMock mapping JSON(只给文件内容)。硬性要求:
1. 只能出现契约里列出的字段,禁止新增字段;
2. 金额字段用整数(单位:分),禁止浮点;
3. 枚举只能取契约给定值;4. 必填字段齐全;
5. 额外生成一条"缺字段"和一条"非法枚举"返回 400 的变体。
【贴上一步 P1 产出的契约表】

拿到后 POST /__admin/mappings 灌进运行中的 WireMock,或存进 mappings/ 进 Git。P2 的输出只保证"格式对、不脑补",能不能当数用还要过最后一步那道契约锁,别拿到就直接信。

第 3 步:一次造三层数据(主线⑥)

"三层"的判法第四章讲过(A 正常/B 边界/C 脏,C 必须全部被正确拒绝且原因对)。这里只补 AI 独有的威力:人编 5 条带业务关联的数据就累,AI 一次给几百条。提示词 P3:

按这个 schema 用 Python(Faker) 生成造数脚本,输出三组 JSONL:
A 正常数据 200 条:格式真实、跨字段关联合理(event_time 晚于 click_time、金额为正整数分);
B 边界数据:金额 0/1/极大值、event_time 恰为第7天0点与23:59:59、超长串、空串;
C 脏数据:缺必填、类型错(金额给字符串)、非法枚举、多一个契约外字段、null、结构错位。
C 组每条额外标注预期被拒原因。

mapping 是规则、JSONL 是数据,数据不手写进规则;成批数据放 __files/ 用 bodyFileName 引用,或由脚本遍历 JSONL 走 Admin API 动态灌(方式 B 的 /convert-to-data-driven 是把 stub 接到 CSV/数据库,同一个道理);回调方向的数据由脚本直接发给被测接口,不经过 mapping。

验收:A/B 必须全过契约校验,C 必须全部被系统拒绝且拒绝原因正确——C 里有一条被当成合法数据,就是真 Bug。

第 4 步:大白话变成 Scenarios 故障(主线⑦,顺带⑨回调)

用业务话描述,让 AI 翻成手工配过的那种 Scenarios 状态机 JSON,外加一份断言清单。提示词 P4:

给我两套 WireMock Scenarios:① 下游前两次 500、第三次 200;② 一直 500。
再列一份"别人调你"的回调清单:正常发一次、立刻重发、错签名、畸形 JSON。
对每种情况写出该断言什么:HTTP 码 / 查账接口笔数 / __admin/requests/count 的次数。

边界不变:重试、幂等、熔断是被测系统的行为,AI 产出的是"注入脚本 + 断言清单",不是替开发实现这些逻辑。

第 5 步:契约锁 + 录制对拍,治住漂移(主线⑩)

AI 越快,"假世界和真世界脱节"越致命。治法是用同一把尺子——第 1 步嚼出来的那份契约 schema——两头量,而且校验用确定性代码,不能再交给 LLM:

  1. 同一把校验器,既校验 AI 生成的 mock 数据,也在 CI 里校验真实响应/抓包流量;
  2. 一次性收集全部违规(缺字段/类型错/非法枚举/契约外字段),别撞到第一个就停——一组看似正常的数据,校验器往往能一次性拦出一串问题,典型的就是这三类:浮点金额(金额必须整数分)、不在值域里的非法枚举、契约里根本没有却私加的字段(比如 coupon_name);bool 是 int 子类、金额单位这类隐蔽项要在锁里显式拦;
  3. 用 WireMock 的录制能力定期对拍:让它以代理模式跑一次真实链路(--record-mappings --proxy-all https://真实下游),把真实响应录下来,和契约、已有 mapping 做 diff,字段一改名立刻暴露。契约对拍不过就卡住合并。
CI 闭环:取契约(AI清洗) → 起WireMock(HTTPS+测试CA) → 灌规则/数据/故障(AI生成,过契约锁)
      → 部署被测(下游指向WireMock) → 跑用例 → 录制/对拍真实响应防漂移 → 数次数/查账 → 销毁

这一步有工业界佐证:一套叫 NL2Test 的方案用"自然语言场景 + HAR"自动切出可回放请求序列,关键设计是让 LLM 做语义理解,而用确定性算法做流量过滤、动态值绑定和断言路径校验,不是让模型直接吐代码;在某大厂 51 个真实场景上精确匹配率 82.4%,9 个月生成 3196 条回归、采纳率 85.4%。(arXiv 工业实践论文)

5.3 新场景:被测系统本身依赖 LLM

你越来越多在测"AI 功能",而 LLM 比传统三方更难测:非确定、慢、贵、限流、返回半截。OpenAI 的接口本质也是 HTTPS,WireMock 照样能 stub 它的 /v1/chat/completions:固定返回某段 JSON 测正常分支、配 429 测限流处理、500/超时测重试与降级、延迟测 loading——这些对韧性最关键的分支零成本、可重复,不用每次真烧钱、也不受模型版本漂移影响。


收尾: 整篇就一个动作:把发往真实依赖的箭头改指到一个完全可控的假服务器,其余全是把这一下做稳。AI 能飞快造出规则、数据、故障,但两样别松手——断言写死、契约锁用代码卡。