MockServer Expectation、代理与请求验证工具手册
第二个请求为什么突然变成 404
支付回调测试第一次返回 202,第二次却收到 404。应用日志只显示“上游不可用”,测试代码也没有改过。打开 MockServer 的请求记录后才发现:expectation 配了 times=1,第一次命中后剩余次数已经归零;另一个偶发失败来自 TTL 到期;还有一组请求虽然 JSON 内容相同,却因 Content-Type 或路径不匹配从未命中。
这类故障正是 MockServer 的强项。它不只是“给路径配一个响应”,而是把一次模拟拆成请求 matcher、动作、优先级、可用次数和存活时间,并把收到的请求写入事件日志供 verify、顺序验证和 retrieve API 检查。MockServer 7.4.0 采用 Apache License 2.0,官方提供 Docker 镜像、独立 JAR、Homebrew、Maven 插件和 Testcontainers 集成;团队模板应锁定服务端和客户端的同一版本线,安装入口与镜像签名方式可在 Docker 指南 和 运行方式 中核对。
7.4.0 起官方发布镜像支持使用项目公钥做 Sigstore 签名验证。拉取后先验证标签对应的镜像,再在内部制品清单记录 digest,避免把可漂移标签当成供应链证据:
docker pull mockserver/mockserver:7.4.0
cosign verify \
--key https://www.mock-server.com/mockserver-cosign.pub \
mockserver/mockserver:7.4.0
docker image inspect mockserver/mockserver:7.4.0 \
--format '{{index .RepoDigests 0}}'运行实验需要 Docker、curl 与 jq;执行镜像签名验证还需要 cosign。代理 HTTPS 流量时会接触测试 CA,录制流量时可能写入 Authorization、Cookie 和业务数据,因此先使用沙箱上游,并把控制 API 只绑定到本机或测试网络。
先启动一个可诊断的实例
单次排障可以直接启动容器:
docker run --name te-mockserver --rm -d \
-p 127.0.0.1:1080:1080 \
--memory=768m \
-e JAVA_TOOL_OPTIONS="-Xmx512m" \
-e MOCKSERVER_LOG_LEVEL=INFO \
-e MOCKSERVER_MAX_EXPECTATIONS=1024 \
-e MOCKSERVER_MAX_LOG_ENTRIES=4096 \
mockserver/mockserver:7.4.01080 同时承载 HTTP、HTTPS、代理流量和控制 API。内存限制不是装饰:expectation 与事件日志都在内存中,响应体和被记录的请求体越大,单条占用越高。MAX_EXPECTATIONS 限制活跃 expectation 窗口,MAX_LOG_ENTRIES 限制 verify 可见的历史窗口;旧记录被环形淘汰后,服务仍能响应,但较晚执行的 verify 可能再也看不到早期请求。官方配置属性还提供按总 body 字节限制事件日志的配置,代理大报文时应同时约束条数、字节数与 JVM 堆。
确认进程已经完成初始化:
curl -i -X PUT http://localhost:1080/mockserver/status
curl -i http://localhost:1080/mockserver/ready状态接口应返回成功响应,readiness 在 initializer 完成前返回 503,完成后返回 200。官方镜像是 distroless,docker exec ... sh 失败是预期行为;排障入口是 docker logs te-mockserver、控制 API 和宿主机上的 curl。
项目里更适合保留 Compose:
services:
mockserver:
image: mockserver/mockserver:7.4.0
ports:
- "127.0.0.1:1080:1080"
environment:
JAVA_TOOL_OPTIONS: "-Xmx512m"
MOCKSERVER_LOG_LEVEL: "INFO"
MOCKSERVER_MAX_EXPECTATIONS: "1024"
MOCKSERVER_MAX_LOG_ENTRIES: "4096"
MOCKSERVER_INITIALIZATION_JSON_PATH: "/config/initializer.json"
MOCKSERVER_FAIL_ON_INITIALIZATION_ERROR: "true"
MOCKSERVER_ATTEMPT_TO_PROXY_IF_NO_MATCHING_EXPECTATION: "false"
MOCKSERVER_REDACT_SECRETS_IN_RECORDED_EXPECTATIONS: "true"
volumes:
- ./mockserver:/config:ro
mem_limit: 768mFAIL_ON_INITIALIZATION_ERROR=true 会把损坏的 JSON、错误 schema 或不可读文件变成启动失败,而不是留下一个“端口已开、expectation 为零”的假健康实例。ATTEMPT_TO_PROXY_IF_NO_MATCHING_EXPECTATION=false 让普通未命中请求停在 404,防止少一条 expectation 就意外访问外网;显式 httpForward 仍可用于受控沙箱。录制脱敏会遮盖常见认证 header,但被遮盖凭证也不能再用于受保护上游回放,因此原始录制只应短暂留在隔离目录。
一个 expectation 如何决定响应
下面的 expectation 只接受严格 JSON、指定 header 和路径;最多命中两次,并在创建后 30 秒失效:
curl -i -X PUT http://localhost:1080/mockserver/expectation \
-H 'Content-Type: application/json' \
-d '{
"id": "payment-authorize-once",
"priority": 20,
"httpRequest": {
"method": "POST",
"path": "/payments/authorize",
"headers": {
"Content-Type": ["application/json"]
},
"body": {
"type": "JSON",
"json": "{\"orderId\":\"o-1001\",\"amount\":99}",
"matchType": "STRICT"
}
},
"httpResponse": {
"statusCode": 202,
"headers": {
"Content-Type": ["application/json"]
},
"body": "{\"status\":\"accepted\"}"
},
"times": {
"remainingTimes": 2,
"unlimited": false
},
"timeToLive": {
"timeUnit": "SECONDS",
"timeToLive": 30,
"unlimited": false
}
}'处理顺序可以理解为四步:先从仍存活且剩余次数大于零的 expectation 中选候选项,再执行 method、path、query、header、cookie 与 body matcher,然后按优先级和创建顺序决定命中项,最后原子地消耗次数并执行 response、forward、callback 或 error 动作。高优先级值先匹配,适合让特殊故障响应盖过通用兜底;依赖“碰巧先创建”的顺序会让初始化重排和并发测试变得脆弱。
字段改变的是不同故障面:
| 字段 | 改动后的直接影响 | 常见误判 |
|---|---|---|
httpRequest.path | 只改变候选请求集合 | 把应用 base path 配错误判为服务未启动 |
body.matchType | STRICT 会拒绝多字段或结构差异 | JSON 看起来相同就认为一定命中 |
priority | 决定多个 matcher 同时成立时谁先执行 | 用创建顺序表达业务优先级 |
times.remainingTimes | 每次命中后递减,归零后失活 | 第二次 404 被当成网络抖动 |
timeToLive | 从 expectation 创建开始计时 | 把测试排队时间排除在 TTL 之外 |
id | 提供更新、清理、审计和按 expectation 验证的稳定身份 | 每轮生成随机 expectation,无法定点回收 |
正向实验:两次命中后精确验证
先在 30 秒内连续请求两次:
for n in 1 2; do
curl -sS -o response.json -w "request=$n status=%{http_code}\n" \
-X POST http://localhost:1080/payments/authorize \
-H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":99}'
cat response.json
done预期两轮都打印 status=202 和 {"status":"accepted"}。然后验证精确次数:
curl -i -X PUT http://localhost:1080/mockserver/verify \
-H 'Content-Type: application/json' \
-d '{
"httpRequest": {
"method": "POST",
"path": "/payments/authorize"
},
"times": {
"atLeast": 2,
"atMost": 2
}
}'验证成功时 HTTP 状态是 202。verify 读取的是事件日志,不是 expectation 的剩余次数:即使 expectation 已耗尽,只要请求记录尚未被环形淘汰,仍可证明这两次调用发生过。
异步应用不要在触发任务后立即赌调度速度。Java 客户端可使用带等待时长的 verify;REST 调用可由测试框架做有上限的轮询。等待上限应来自业务异步 SLA,超时后保留 MockServer retrieve 输出与应用日志,而不是无限 sleep。
反向实验:稳定暴露 matcher、times 与 TTL
第三次发送同一请求:
curl -i -X POST http://localhost:1080/payments/authorize \
-H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":99}'预期返回 404,因为两次额度已经耗尽。重新创建 expectation 后,把金额改成字符串:
curl -i -X POST http://localhost:1080/payments/authorize \
-H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":"99"}'严格 JSON matcher 下仍应返回 404。如果在创建后等待超过 TTL 再发送正确请求,也应得到 404。这三个 404 分别对应“次数耗尽”“字段类型不匹配”“生命周期结束”,不能只凭状态码诊断。
取回请求与日志证据:
curl -sS -X PUT \
'http://localhost:1080/mockserver/retrieve?type=REQUESTS&format=JSON' \
-H 'Content-Type: application/json' \
-d '{"path":"/payments/authorize"}'
curl -sS -X PUT \
'http://localhost:1080/mockserver/retrieve?type=LOGS&format=JSON' \
-H 'Content-Type: application/json' \
-d '{"path":"/payments/authorize"}'DEBUG 日志会给出逐字段 matcher 差异,适合短时排障;长期保持 DEBUG 会放大 CPU、日志和敏感数据风险。先看实际 method、path、header 与 body,再判断是 matcher 过严、客户端序列化变化,还是 expectation 已失活。
initializer 把测试状态变成代码
将稳定 expectation 写入 mockserver/initializer.json:
[
{
"id": "catalog-get-product",
"priority": 10,
"httpRequest": {
"method": "GET",
"path": "/catalog/products/p-1001"
},
"httpResponse": {
"statusCode": 200,
"headers": { "Content-Type": ["application/json"] },
"body": "{\"id\":\"p-1001\",\"stock\":8}"
}
}
]JSON initializer、classpath initializer、OpenAPI initializer 和 Maven 插件 initializer 的行为与装载方式可在 Expectation Initializers 核对。JSON 文件适合跨语言共享;Java initializer 适合复用构造器和类型;测试运行时通过客户端创建的 expectation 适合每个用例独有的数据。
initializer 不等于热更新。默认在启动时读取,改文件后需要重启;开启 watch 时,应使用稳定 id,并验证更新与删除语义。CI 必须探测 /mockserver/ready,不能只检查 TCP 端口。故意删掉 JSON 末尾括号并重启,可验证 FAIL_ON_INITIALIZATION_ERROR=true 会让容器启动失败,日志应指向解析错误;修复文件后再启动,readiness 才能恢复为 200。
verify 要验证调用契约而非业务返回
业务接口返回成功,只能证明应用自己的最终结果,不能证明它给依赖发对了请求。关键副作用至少验证次数、关键字段和必要顺序:
curl -i -X PUT http://localhost:1080/mockserver/verifySequence \
-H 'Content-Type: application/json' \
-d '{
"httpRequests": [
{ "method": "POST", "path": "/payments/authorize" },
{ "method": "POST", "path": "/payments/capture" }
]
}'成功返回 202;顺序不存在时返回 406 并给出未满足的验证信息。顺序验证要求匹配请求按给定次序出现,但中间可以存在其他请求。需要严格证明“没有额外调用”时,再分别验证精确次数或用 retrieve 输出比对。
并行测试共享同一事件日志,reset 会删除其他用例的 expectation 和记录,固定路径也会让两个用例互相消费 times。可靠做法按隔离强度排序:
每个测试类或 worker 启动独立 Testcontainers 实例并使用动态端口。同一实例内给 path、header 或 tenant id 加测试运行标识,并让 verify 使用相同标识。只清理带稳定 id 或本用例 matcher 的对象,不在并发阶段调用全局 reset。
异步任务完成后再 verify,失败时先取证,最后清理。
测试接入的生命周期应是“启动实例、等待 ready、创建本用例 expectation、注入 base URL、运行应用、verify、保存失败证据、清理”。Java 项目可使用 MockServer Testcontainers 模块或 @MockServerTest;Node.js、Go、Python 项目可以直接调用 REST API。动态端口必须由测试夹具注入应用,禁止在并行 CI 中假定 1080 永远空闲。
代理与 TLS 会扩大信任面
MockServer 可将未被 expectation 命中的流量转发到沙箱上游,也可以对特定请求使用 httpForward。代理入门、录制和 HAR 导出见 Proxying 指南。使用显式 forward 比“所有未命中请求自动外发”更容易审计:
curl -i -X PUT http://localhost:1080/mockserver/expectation \
-H 'Content-Type: application/json' \
-d '{
"id": "forward-sandbox-catalog",
"httpRequest": {
"method": "GET",
"path": "/sandbox/catalog/.*"
},
"httpForward": {
"host": "sandbox.example.internal",
"port": 443,
"scheme": "HTTPS"
}
}'所有经 httpForward 或代理路径转发的请求都会进入 recorded expectations。发送一条合成请求后先导出 JSON,再撤销 forward expectation,最后把导出结果装回当前独占实例:
curl -fsS \
'http://localhost:1080/sandbox/catalog/products/p-1001' \
-H 'X-Test-Run: mockserver-recording-001'
curl -fsS -X PUT \
'http://localhost:1080/mockserver/retrieve?type=RECORDED_EXPECTATIONS&format=JSON' \
-H 'Content-Type: application/json' \
-d '{
"method": "GET",
"path": "/sandbox/catalog/products/p-1001",
"headers": { "X-Test-Run": ["mockserver-recording-001"] }
}' > mockserver-recording-review.json
jq -e 'length > 0' mockserver-recording-review.json
curl -fsS -X PUT \
'http://localhost:1080/mockserver/clear?type=EXPECTATIONS' \
-H 'Content-Type: application/json' \
-d '{"path":"/sandbox/catalog/.*"}'
curl -fsS -X PUT http://localhost:1080/mockserver/expectation \
-H 'Content-Type: application/json' \
--data-binary @mockserver-recording-review.json
curl -i -sS \
'http://localhost:1080/sandbox/catalog/products/p-1001' \
-H 'X-Test-Run: mockserver-recording-001'最后一次请求必须在 forward 已删除、未命中自动代理已关闭时仍返回录制的状态码和关键字段,才证明结果可离线上游回放。导出的 matcher 往往会锁死 request id、时间戳或 session token;入库前要用合成值替换,收紧允许外发的 host,并用一条应命中、一条应 404 的请求证明没有录成“接收一切”的规则。HAR 与录制 JSON 都可能含完整响应,不能因为认证 header 已遮盖就跳过字段级脱敏。
HTTPS 代理不是透明旁路。MockServer 会终止客户端 TLS、读取 HTTP 内容以匹配和记录,再与上游建立 TLS 连接,因此客户端必须信任测试 CA;其安全含义与受控的中间人代理相同。动态 CA、固定证书、上游证书校验和 mTLS 配置应按 HTTPS 与 TLS 操作。测试 CA 只能进入测试信任库,不要导入系统全局或生产镜像。
共享实例若要求所有 HTTPS 请求都出示客户端证书,可在 Compose 中挂载专用测试证书并启用 mTLS。私钥必须是 PKCS#8,服务端证书文件包含完整链;PROACTIVELY_INITIALISE_TLS 让证书或私钥错误在启动阶段暴露:
environment:
MOCKSERVER_TLS_MUTUAL_AUTHENTICATION_REQUIRED: "true"
MOCKSERVER_TLS_MUTUAL_AUTHENTICATION_CERTIFICATE_CHAIN: /certs/ca.pem
MOCKSERVER_TLS_PRIVATE_KEY_PATH: /certs/server-key-pkcs8.pem
MOCKSERVER_TLS_X509_CERTIFICATE_PATH: /certs/server-cert-chain.pem
MOCKSERVER_PROACTIVELY_INITIALISE_TLS: "true"
volumes:
- ./local-secrets/mockserver-certs:/certs:ro先做有客户端证书的正向请求,再分别省略客户端证书和 TLS。预期第一条进入目标 expectation,第二条在握手阶段失败,第三条返回 426 Upgrade Required:
curl --cacert local-secrets/mockserver-certs/ca.pem \
--cert local-secrets/mockserver-certs/client-cert.pem \
--key local-secrets/mockserver-certs/client-key-pkcs8.pem \
-H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":99}' \
https://localhost:1080/payments/authorize
curl --cacert local-secrets/mockserver-certs/ca.pem \
-H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":99}' \
https://localhost:1080/payments/authorize
curl -i -H 'Content-Type: application/json' \
-d '{"orderId":"o-1001","amount":99}' \
http://localhost:1080/payments/authorize如果只想保护 expectation、verify、retrieve、clear 等管理入口,而业务 mock 流量仍使用普通 TLS,应改用 MOCKSERVER_CONTROL_PLANE_TLS_MUTUAL_AUTHENTICATION_REQUIRED 与对应的 control-plane CA 配置;不要启用全局 mTLS 后再让业务客户端跳过证书校验。
控制面与代理数据面的认证是两件事。创建、清理、verify、retrieve 等控制 API 可配置 mTLS 或 JWT;代理客户端认证使用 Proxy-Authorization。共享测试环境应启用控制面保护、网络访问控制和审计,详细能力见 Control Plane Authentication。
录制文件、HAR、Dashboard 和 retrieve 输出可能包含 bearer token、Cookie、身份证明、订单、地址与错误栈。只能代理沙箱,使用合成账号,导出后按字段脱敏,设置短保留期,并把录制目录排除在版本控制和普通 CI artifact 之外。
清理、回滚与故障证据
定点清理匹配对象:
curl -i -X PUT http://localhost:1080/mockserver/clear \
-H 'Content-Type: application/json' \
-d '{"path":"/payments/.*"}'全量恢复空状态只用于独占实例:
curl -i -X PUT http://localhost:1080/mockserver/reset
docker rm -f te-mockserver
rm -f mockserver-recording-review.json失败时按这个顺序保留证据:应用请求日志中的目标 URL 与 trace id、MockServer retrieve 的请求、matcher 差异日志、active expectations、容器启动日志。随后才 clear 或 reset。先 reset 再排障会同时删除最有价值的事件日志。
常见现象可以沿因果链定位:
| 现象 | 第一证据 | 常见原因 | 修复动作 |
|---|---|---|---|
返回 404 | active expectations 与 retrieve logs | matcher 不符、times 耗尽、TTL 到期 | 对照字段差异并重建 expectation |
verify 返回 406 | verify 响应体与 recorded requests | 次数、顺序或异步时序不满足 | 等待业务完成,收紧 matcher |
readiness 长期 503 | 容器启动日志 | initializer 慢或解析失败 | 修复文件,保持 fail-fast |
| HTTPS 握手失败 | 客户端 TLS 错误与证书链 | 未信任测试 CA、SNI 或 mTLS 配错 | 使用专用测试 truststore |
代理返回 502 | forward 日志与上游连通性 | DNS、代理、上游 TLS 或超时 | 在容器网络内验证上游地址 |
| 偶发找不到早期请求 | maxLogEntries 与请求量 | 事件日志已环形淘汰 | 增大窗口或缩小单实例并发量 |
| 容器 OOM | 内存曲线、body 大小、日志条数 | 大 body 被长期记录 | 限制 body 字节、日志窗口和堆 |
部署形态与长期治理
嵌入式实例启动快、与单测生命周期一致,但会把 MockServer 依赖带进测试进程;Testcontainers 隔离清晰、端口动态,代价是 Docker 启动时间;Compose 适合开发联调和可重复 initializer;Kubernetes 或共享实例适合多应用联调,却必须解决控制面认证、租户隔离、日志归属和容量竞争。MockServer 不是生产上游的高可用替身,也不应成为长期存放业务契约的唯一仓库。
容量预算要同时看活跃 expectation 数、请求速率、平均与高分位 body 大小、事件日志保留窗口、并行 worker 数和 verify 最长延迟。可观测判据不是一个脱离负载的固定阈值,而是:连续测试轮次后活跃 expectation 回到基线,事件日志淘汰不早于最长 verify 窗口,堆使用不随轮次单调增长,控制面变更能追溯到 owner。
团队仓库至少要把这些对象纳入评审:
mockserver/initializer.json
test-support/mockserver-client.*
test-support/mockserver-verify.*
test-support/mockserver-clean.*每个长期 expectation 使用稳定 id,写明业务意图、owner、允许次数、TTL 与退出条件;复杂 regex 和 JSON matcher 必须有一条会命中与一条不会命中的测试。版本升级先在临时实例回放 initializer 和正反实验,再核对 matcher、TLS、代理录制与客户端兼容性。MockServer 证明的是测试期间观察到的 HTTP 交互;跨版本接口兼容仍要由 OpenAPI、消费者契约和真实沙箱 smoke 共同保证。
