WireMock HTTP Stub、录制回放与未命中排查工具手册
同一个 404,可能来自三条完全不同的链路
支付回调测试突然返回 404。开发者检查了 URL,重启了 mock,甚至把 matcher 改成 .*,结果 CI 仍失败。此时最重要的不是继续放宽规则,而是先分清:请求根本没到 WireMock、请求到了却没有匹配 stub,还是匹配后的响应文件或场景状态出错。
WireMock 会把收到的 HTTP 请求与 stub mappings 匹配,选择优先级最高且条件满足的响应,并把 serve event 写入内存 request journal。匹配失败时,业务端通常得到 404;journal、unmatched requests 和 near-miss 信息则保留了 method、path、query、headers、body 与候选规则的差异。它因此适合模拟不稳定或尚未可用的 HTTP 上游,也适合把超时、错误码和有状态交互变成稳定测试条件。
团队基线可锁定 WireMock 3.13.2。官方同时发布 standalone JAR、Docker 镜像和 JVM 测试依赖;4.x 仍处于 beta,不宜在没有兼容性验证时替换稳定线。下载页列出稳定版与 beta 版的实际坐标。WireMock Java 采用 Apache License 2.0,开源社区不承诺 SLA;需要商业托管、集中协作或支持时,应把 WireMock Cloud 或服务虚拟化平台作为另一种部署形态评估,而不是把单机开源进程包装成高可用控制面。
standalone JAR 运行在 Java 11 或更高版本,Java 17 也是官方快速入门列出的选择;不要把 3.x 错写成必须 Java 17。Docker 路径由官方镜像携带运行时,更适合开发机和 CI。两种入口都要锁版本,镜像还应在团队供应链中记录 digest。
从空目录启动 standalone 或 Docker
非容器环境从 Maven Central 下载入口取得 wiremock-standalone-3.13.2.jar,先核对来源和校验值,再启动:
java -version
java -jar wiremock-standalone-3.13.2.jar \
--port 8082 \
--root-dir ./wiremock \
--verbose--root-dir 下的 mappings/ 保存 JSON stub,__files/ 保存由 bodyFileName 引用的响应体。standalone 默认以当前目录为 root;路径写错时服务仍可能启动,但一个 stub 都不会加载。
项目级 Compose 可以这样固定:
services:
wiremock:
image: wiremock/wiremock:3.13.2
ports:
- "127.0.0.1:8082:8080"
command:
- "--verbose"
- "--max-request-journal-entries"
- "1000"
volumes:
- ./wiremock/mappings:/home/wiremock/mappings:ro
- ./wiremock/__files:/home/wiremock/__files:ro容器内 root 固定为 /home/wiremock。只读挂载适合运行已评审的 stub,也意味着 POST /__admin/mappings/save 和录制持久化不能写回目录;录制应使用独立的可写工作目录,审查后再把文件复制到正式目录。
启动后检查进程、版本和健康状态:
docker compose up -d wiremock
curl -fsS http://127.0.0.1:8082/__admin/version
curl -fsS http://127.0.0.1:8082/__admin/health
curl -fsS http://127.0.0.1:8082/__admin/mappings | jq '.meta.total'预期版本为 3.13.2、健康状态为 healthy。如果 health 成功而 mapping 总数为 0,问题在目录或 JSON 加载,不在端口。容器日志中的 mapping 解析错误应作为第一证据保留。
把第一条 stub 写成可审查资产
先创建响应体 wiremock/__files/payment-paid.json:
{
"paymentId": "pay-test-001",
"status": "PAID"
}再创建 wiremock/mappings/payment-status-paid.json:
{
"name": "payment status is paid",
"priority": 10,
"metadata": {
"upstream": "payment-sandbox",
"owner": "payment-team",
"case": "paid"
},
"request": {
"method": "GET",
"urlPath": "/payment/status",
"queryParameters": {
"paymentId": {
"equalTo": "pay-test-001"
}
},
"headers": {
"Accept": {
"contains": "application/json"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "payment-paid.json",
"fixedDelayMilliseconds": 80
}
}几个字段决定了故障形态:
| 字段 | 运行时作用 | 典型错配 |
|---|---|---|
priority | 数值越小优先级越高;同类宽泛规则应放在更低优先级 | fallback 抢先命中,正确业务规则永远不执行 |
urlPath | 只匹配路径,query 由 queryParameters 独立判断 | 使用完整 url 后,参数顺序变化导致未命中 |
bodyPatterns | JSON、XML、正则等正文匹配器决定请求是否合格 | 把动态 ID 写进 equalToJson,测试随机失败 |
bodyFileName | 从 root 下的 __files 读取响应体 | 文件名大小写或挂载路径错误,返回体加载失败 |
metadata | 给 stub 标注来源、owner 和场景,便于管理 API 查询 | stub 能运行,却没人知道何时更新或删除 |
fixedDelayMilliseconds | 在响应阶段注入固定延迟 | 延迟超过客户端超时,表面像网络故障 |
name 与 metadata 不参与业务匹配,但决定团队能否治理几百条 stub。响应体较大或需复用时放入 __files;短小且只用一次的 JSON 可以用 jsonBody。二进制文件进入仓库前仍要检查许可、大小和敏感信息。
正向实验:命中、计数和 journal 形成闭环
重启以加载静态文件,然后发起严格匹配的请求:
docker compose restart wiremock
curl -i -sS \
-H 'Accept: application/json' \
'http://127.0.0.1:8082/payment/status?paymentId=pay-test-001'预期状态码为 200,响应 JSON 中 status 为 PAID。再查询匹配次数与请求记录:
curl -fsS -X POST http://127.0.0.1:8082/__admin/requests/count \
-H 'Content-Type: application/json' \
-d '{
"method": "GET",
"urlPath": "/payment/status",
"queryParameters": {
"paymentId": { "equalTo": "pay-test-001" }
}
}' | jq .count
curl -fsS http://127.0.0.1:8082/__admin/requests \
| jq '.requests[] | {method: .request.method, url: .request.url, wasMatched: .wasMatched}'预期 count 至少增加 1,对应 serve event 的 wasMatched 为 true。这比“curl 得到 200”多证明了一步:响应确实来自目标 matcher,而不是另一条宽泛 stub。
动态创建的 mapping 只存在于运行时,重启或 reset 后可能消失;静态 mapping 从磁盘加载,是团队可重复的基线。需要长期保留动态 stub 时,先审查内容,再调用保存 API并确认挂载目录可写;只读运行实例不应开放这一能力。
反向实验:稳定制造未命中并读取差异
把 query 值改错,同时移除必需的 Accept:
curl -i -sS \
'http://127.0.0.1:8082/payment/status?paymentId=pay-test-999'
curl -fsS http://127.0.0.1:8082/__admin/requests/unmatched \
| jq '.requests[] | {method, url, headers}'预期业务请求返回 404,unmatched 列表出现 paymentId=pay-test-999,而且 headers 中没有需要的 Accept: application/json。verbose 日志还会打印最接近 stub 的差异。这个反例把“mock 坏了”拆成两个可修复事实:query 值不相等,header matcher 也不满足。
排障时不要立即删除 matcher。先按以下顺序保留证据:
在应用启动日志确认 base URL 真正指向当前 WireMock。用 /__admin/requests 判断请求是否到达;没有记录时检查 DNS、端口、代理和容器网络。有 unmatched 记录时比较 method、path、query、headers 和 body。
有 matched 记录但响应异常时检查优先级、scenario state、bodyFileName、transformer 与延迟。修正规则后重复同一个反例,确认 unmatched 数不再增加且目标 count 增加。
request journal 是内存日志。--no-request-journal 会让验证和未命中查询失去基础;--max-request-journal-entries 则限制保留条数,旧记录会被淘汰。日常联调应设置有界 journal,高流量压测才考虑关闭,并用其他访问日志补足证据。
用 scenario 模拟状态,而不是堆随机响应
轮询、重试和异步任务经常需要“第一次处理中,第二次成功”。WireMock scenario 是一个进程内状态机,初始状态固定为 Started。第一条 mapping 要求 Started 并把状态改为 completed:
{
"scenarioName": "payment polling pay-test-001",
"requiredScenarioState": "Started",
"newScenarioState": "completed",
"request": {
"method": "GET",
"urlPath": "/payment/poll"
},
"response": {
"status": 202,
"jsonBody": { "status": "PROCESSING" }
}
}第二条 mapping 要求 completed 并返回成功:
{
"scenarioName": "payment polling pay-test-001",
"requiredScenarioState": "completed",
"request": {
"method": "GET",
"urlPath": "/payment/poll"
},
"response": {
"status": 200,
"jsonBody": { "status": "PAID" }
}
}连续请求两次,预期依次得到 202/PROCESSING 和 200/PAID。状态保存在 WireMock 进程内,不在 mapping 文件里;并发测试共享 scenario 名时会相互推进状态。测试 teardown 应执行:
curl -fsS -X POST http://127.0.0.1:8082/__admin/scenarios/reset
curl -fsS http://127.0.0.1:8082/__admin/scenarios | jq .并发 CI 更适合每个测试进程独立 WireMock,或为每个 case 生成唯一 scenario 名。共享实例上的全局 reset 会影响其他测试。
项目与测试生命周期要绑定
应用通过环境变量切换真实沙箱和 WireMock:
external:
payment-base-url: ${PAYMENT_BASE_URL:http://wiremock:8080}
payment-timeout-ms: ${PAYMENT_TIMEOUT_MS:2000}开发和测试默认值可以指向 mock,生产配置必须显式提供真实地址。启动日志打印最终 scheme、host 与 port,不打印 query token、Authorization 或完整 URL。真实沙箱 smoke、provider contract 或 OpenAPI 校验仍需保留,因为 WireMock 只会忠实执行仓库里的假设,无法证明提供方真的接受这些请求。
JVM 单元或集成测试可直接引入稳定线依赖,并使用动态端口避免并行冲突:
testImplementation "org.wiremock:wiremock-standalone:3.13.2"@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();测试内创建 stub,业务客户端使用 wm.baseUrl(),afterEach 重置 mappings、requests 和 scenarios。非 JVM 项目可把官方容器作为 CI service:job 启动后轮询 /__admin/health,测试结束总是收集脱敏后的 unmatched 摘要并销毁容器。不要让多个并行 job 共用固定端口、journal 和 scenario state。
录制和代理先隔离,再生成可评审 stub
录制把真实交互快速转成 mapping,也最容易把 Authorization、Cookie、JWT、客户数据和偶然响应写进仓库。安全做法是使用专用 sandbox 账号与独立可写目录,先代理并观察 journal,再用 snapshot 生成非持久结果。
创建一条低优先级 proxy mapping,并保留它的 ID,后续才能在不影响其他 stub 的前提下撤销代理:
curl -fsS -X POST http://127.0.0.1:8082/__admin/mappings \
-H 'Content-Type: application/json' \
-d '{
"priority": 100,
"request": {
"method": "ANY",
"urlPathPattern": "/sandbox-api/.*"
},
"response": {
"proxyBaseUrl": "https://sandbox-api.example.test"
}
}' > wiremock-proxy-mapping.json
PROXY_MAPPING_ID=$(jq -r '.id' wiremock-proxy-mapping.json)
test -n "$PROXY_MAPPING_ID" && test "$PROXY_MAPPING_ID" != "null"发送少量构造数据流量后,按路径取 snapshot:
curl -fsS -X POST http://127.0.0.1:8082/__admin/recordings/snapshot \
-H 'Content-Type: application/json' \
-d '{
"filters": {
"urlPathPattern": "/sandbox-api/.*"
},
"captureHeaders": {
"Accept": {},
"Content-Type": { "caseInsensitive": true }
},
"requestBodyPattern": {
"matcher": "auto",
"ignoreArrayOrder": false,
"ignoreExtraElements": true
},
"extractBodyCriteria": {
"textSizeThreshold": "2 kb",
"binarySizeThreshold": "1 Mb"
},
"outputFormat": "FULL",
"persist": false,
"repeatsAsScenarios": false
}' > wiremock-recording-review.json
jq -e '.mappings | length > 0' wiremock-recording-review.jsonoutputFormat:FULL 返回生成后的完整 mappings,便于审查和按 ID 清理;persist:false 不把它们保存到正式 mappings 后端,但生成结果仍会加入当前进程,直到 reset、删除或进程退出。captureHeaders 只把内容协商字段变成 matcher,避免把认证 header 固化。extractBodyCriteria 决定大响应是否拆到 __files,并不等于敏感信息过滤。录制后必须检查 request journal、snapshot JSON 和可能生成的响应文件:删除 Authorization、Proxy-Authorization、Cookie、Set-Cookie、token、邮箱、手机号、地址、订单号和不可复用 ID;把真实值替换成语义化假数据;收紧 matcher;添加 name、metadata 和 owner;再进入代码评审。
审查通过后先删除代理 mapping,再重放同一个请求。此时返回仍应来自刚生成的本地 stub;如果变成 404,说明 snapshot 的 matcher 没覆盖真实请求;如果仍访问上游,说明代理没有被正确撤销:
curl -fsS -X DELETE \
"http://127.0.0.1:8082/__admin/mappings/$PROXY_MAPPING_ID"
curl -i -sS \
'http://127.0.0.1:8082/sandbox-api/catalog/products/p-1001'只有在撤销代理后仍能得到预期状态码、响应字段,而且 journal 中该请求命中生成的 stub,录制才形成回放闭环。不要用“上游恰好仍在线”代替这个证据。
--proxy-all --record-mappings 适合个人快速探索,却会把收到的流量直接持久化,团队环境更容易误收全量数据。录制完成后按 snapshot 返回的 ID 删除未采用的运行时 mappings,清 journal、撤销 sandbox 凭证,并销毁录制目录中未入库的原始文件。CI 默认不启用 proxy 或 recorder,避免测试在 stub 缺失时悄悄访问外网。
HTTPS、Admin API 与业务 stub 是三层入口
启用 --https-port 8443 后,WireMock 仍会保留默认 HTTP 监听,除非同时使用 --disable-http。未提供 keystore 时会使用自签名证书,客户端常见证据是 PKIX path building failed;正确修复是给测试客户端安装专用 CA 或提供受信 keystore,不是全局关闭证书校验。
ports:
- "127.0.0.1:8443:8443"
command:
- "--https-port"
- "8443"
- "--disable-http"
- "--https-keystore"
- "/run/secrets/wiremock-keystore.p12"
- "--keystore-type"
- "PKCS12"
- "--keystore-password"
- "${WIREMOCK_KEYSTORE_PASSWORD:?set WIREMOCK_KEYSTORE_PASSWORD}"
volumes:
- ./local-secrets/wiremock-keystore.p12:/run/secrets/wiremock-keystore.p12:ro示例占位值必须由运行时 secret 注入,不能原样投入共享环境。需要双向 TLS 时,再配置 truststore 与 --https-require-client-cert。WireMock 作为 HTTPS proxy 时,客户端到 WireMock、WireMock 到上游是两段独立 TLS 会话;上游信任、客户端证书和 WireMock 服务端证书需要分别建模。
standalone 的 --admin-api-basic-auth <user:password> 能保护 /__admin,--admin-api-require-https 能拒绝通过 HTTP 调用 Admin API。它们不自动保护普通 stub URL。共享实例还需网络策略或反向代理限制业务 mock 入口。凭证作为命令行参数可能出现在进程参数和诊断信息中;严格环境更适合由受控反向代理完成身份认证,并让 WireMock 只监听内部网络。
Admin API 能新增、删除、reset mappings,读取带正文的 journal,启动录制,甚至关闭服务。把 /__admin 裸露给所有联调人员,相当于允许任何人改变测试结果与读取敏感请求。每项目独立实例通常比在一个大实例中模拟 namespace 更容易审计和回滚。
扩展能力要按供应链依赖管理
自定义 matcher、response transformer、webhook、GraphQL 等能力可以通过扩展 JAR 加载。Docker 中把扩展挂载到 /var/wiremock/extensions,再用类名启用:
volumes:
- ./wiremock/extensions:/var/wiremock/extensions:ro
command:
- "--extensions"
- "org.wiremock.webhooks.Webhooks"扩展与 WireMock 核心版本、Java 版本和依赖树存在兼容关系。仓库应记录扩展坐标、版本、SHA-256、许可证和用途;升级先运行正反实验,再更新镜像。不要从临时 URL 下载 JAR 后直接挂到共享实例。响应模板能减少重复 JSON,但全局模板会增加 CPU 开销,也可能把请求 header 或 body 回显到响应;默认采用局部模板,并对可用变量做安全审查。
容量、清理与回滚
WireMock 的主要运行成本不是静态 JSON,而是 request journal、响应体、模板渲染、延迟/fault 注入和共享并发。journal 保存完整请求时可能同时消耗堆内存并留存凭证或个人信息。容量测试应观察堆使用量、GC、请求延迟、unmatched 比例和 journal 淘汰速度;1000 只是本地示例值,团队上限应由请求大小、并发和排障窗口测量得到。
不同清理 API 的影响不同:
# 只清请求日志
curl -fsS -X DELETE http://127.0.0.1:8082/__admin/requests
# 只把 scenario 恢复到 Started
curl -fsS -X POST http://127.0.0.1:8082/__admin/scenarios/reset
# mappings 与 request journal 一起恢复到启动时基线
curl -fsS -X POST http://127.0.0.1:8082/__admin/reset共享实例禁止在普通测试里调用全局 reset。测试应拥有独立实例,或只删除带本次 metadata 的动态 mappings 和请求。静态目录回滚使用 Git 恢复上一组已评审 mappings 与 __files,重启后执行 health、mapping 数、正向命中和反向未命中实验。动态 Admin API 变更没有可靠审计与版本历史,不应成为长期配置来源。
停止本地实例:
docker compose down
rm -f wiremock-recording-review.json wiremock-proxy-mapping.json停用共享实例还要撤销 Admin 凭证、sandbox token、TLS 私钥和网络入口,删除未脱敏 journal/录制 artifact,并确认应用配置不会回退到生产地址。长期 stub 应有来源、owner、契约修订标识和真实 sandbox smoke;字段或错误码变化时,同步更新 OpenAPI/契约与 WireMock,而不是继续让旧响应“稳定通过”。
推广前逐项确认
稳定线锁定为 WireMock 3.13.2 或经兼容验证的后续稳定版,未使用 latest、beta。standalone 使用 Java 11+;Docker 镜像记录版本、digest、来源和 Apache 2.0 许可。mappings 与 __files 路径正确,正式运行挂载只读,录制使用独立可写目录。
正向请求能证明目标 stub 命中,反向请求能在 unmatched/journal 中看到字段差异。scenario 在测试结束恢复,并发任务不共享状态机和全局 reset。request journal 有容量上限,禁用时有替代证据,失败 artifact 已脱敏。
录制仅使用 sandbox 假数据,snapshot 先设为非持久,入库前删除凭证和个人信息。HTTPS、Admin API 认证与普通 stub 入口分别受控,私钥和密码未进入仓库或日志。扩展 JAR 已锁版本、校验值和许可证,并通过核心版本兼容测试。
CI 每个 job 独立启动、检查 health、收集脱敏证据并销毁实例。rollback 能恢复静态基线,退出时会撤销代理、凭证、录制文件和网络入口。
