本地联调卡住的原因通常不是「收不到回调」—— 那件事用内网穿透十分钟就解决了。真正拖慢进度的是另外两件:一个真实账号怎么给五个人同时用,以及每验证一次改动都要手工去发条消息。穿透、Mock、录制回放解决的是三个不同的问题,混着用就会变成「隧道搭得很漂亮,回归还是靠人点」。这篇讲这三件工具各自的位置、边界和补齐顺序,示例按 wecomapi 的接入形态组织。
三个阶段,三种需求
先把分工说清楚,后面的取舍才有依据。
- 写代码时要的是快、不依赖外部 —— 用 Mock。改一行代码就要等一条真实消息进来,反馈环长到没法写代码。
- 联调时要的是真链路 —— 用内网穿透。只有真报文才能暴露解析、验签和字段假设上的问题。
- 回归和 CI 要的是可重复、无人值守 —— 用录制回放。真链路做不到无人值守,真人也不该每天点同样的用例。
多数团队只上了第二件,于是每个分支都靠人工发消息验证,联调时间随用例数线性增长。补齐顺序建议倒过来:先有 Mock 让单测跑起来,再搭穿透打通真链路,再把联调期抓到的真实报文沉淀成回放夹具 —— 第三步几乎是白捡的,那些报文本来就从你眼前流过了。
判断自己卡在哪一环有个简单办法:问「改一行代码到看见效果要多久」。超过一分钟,缺的是 Mock;每次都要找同事配合发条消息,缺的是入向注入;每次发版前都得手工回归一遍,缺的是回放。三个症状对应三件工具,别用加班去补。
内网穿透的四个坑
穿透工具本身很简单,坑都在它和回调机制的交界处。
- 1隧道地址每次重启都变。免费隧道给的是随机子域,一变就得回控制台改配置,一天改三次没人受得了。要么用固定子域,要么自己拿一台有公网 IP 的机器跑反向代理,把地址钉死。
- 2回调地址是全局单点,多人开发必然互相抢。这是最大的一个坑,下一节单独讲。
- 3隧道会改写请求头。转发链、协议头和 Host 都可能变,如果验签或路由依赖这些字段,就会出现「本地失败、线上正常」的诡异现象。排查时先确认你拿到的是不是原始 body 字节。
- 4本地打断点会触发平台重投。你在断点上停了三十秒,平台早就判超时并重投了两次,放行之后你会看到同一条事件被处理三遍。本地也必须先快速 ACK 再处理,和线上口径保持一致。
选穿透工具时真正值得比较的只有两件事:地址能不能固定,以及有没有请求查看面板。前者决定你一天要不要改三次配置,后者决定你能不能看到平台发来的原始报文 —— 排查验签问题时,直接看字节比翻任何日志都快。其余那些花哨功能在这类场景里都用不上。
第四条顺带证明了一件事:本地环境如果没有幂等,联调阶段就会暴露出来 —— 这是好事。别用「本地先关掉幂等方便调试」把这个信号盖掉,那等于把问题推到线上再发现。
一个回调地址,五个开发者
一个账号通常只能配一个回调地址,而团队有五个人。轮流改配置的代价不只是麻烦,还包括「谁改的没人知道」和「改完忘了改回去」,后者会让整条测试链路静默停摆半天,而且不会有任何报错。环境之间怎么隔离(每套环境一个独立账号、凭证与落库各自分开)站内讲多环境管理那篇写过,这一节只解决同一套联调环境里多个人抢一个地址的问题。
可行的做法是加一层扇出:在测试环境部署一个常驻的小服务,把它的地址填成那个唯一的回调地址,它只做三件事 —— 验签、快速 ACK、按规则把事件复制给各个开发者的隧道。用 wecomapi 的事件回调时这一层就是普通的 HTTP 转发,不需要理解事件语义,几十行代码的量级。
app.post("/callback", raw(), async (req, res) => {
if (!verify(req)) return res.sendStatus(401); // 真实密钥只存在这一层
res.sendStatus(200); // 先 ACK,不等下游
for (const tunnel of targets(req)) { // 广播 / 按会话 / 按账号
fetch(tunnel, { method: "POST", body: req.body }).catch(noop);
}
});- 1全量广播:每个人都收到全部事件,实现最简单,适合三五个人。缺点是所有人的本地都会对同一条事件做出反应,必须配合出向白名单才安全。
- 2按会话路由:给每个开发者分配固定的测试会话或测试客户,只把命中的事件转给他。干扰最小,代价是维护一张映射表。
- 3按账号路由:多账号托管时最自然,一人一个测试账号,隔离最彻底,成本是账号数量。
扇出层还能顺手办一件事:既然所有事件都要经过它,就让它在转发的同时把原始报文带时间戳落一份到磁盘。联调期结束,夹具库的原料自动就有了,不用谁专门去收集。存储成本可以忽略,价值在最后一节。
三选一的依据是「你们有几个可用的测试账号」:够就按账号隔离,不够就按会话路由,人少且都很自觉才用广播。这层还有个附带好处 —— 它是唯一持有真实回调密钥的地方,开发者本地不需要配置任何生产密钥。代价是它成了新的单点,记得给它配一条最简单的存活检查,否则它半夜挂了,第二天全组都在查「为什么收不到事件」。
Mock 要分两层,别只 Mock 出向
Mock 的价值不在于把 happy path 跑通 —— 真链路也能跑通 happy path。它的价值在于让失败分支可以被稳定复现,而失败分支才是上线后真正会出事的地方。
- 出向 Mock(你调平台):一个可编程的假服务,至少覆盖成功、参数错误、频控、超时,以及「返回失败但实际已生效」这五类。频控和超时在真实环境里你根本约不出来,只能造。
- 入向注入(平台调你):一个能按模板往本地回调接口灌事件的小工具。它比在手机上手动发消息快两个数量级,也是唯一能构造「同一会话瞬间十条」这类并发场景的办法。
入向注入用的模板最好直接来自真实报文,而不是照着文档手写。手写模板会把你对 wecomapi 事件结构的理解偏差一起固化进测试里,于是测试全绿、线上照崩 —— 这种测试比没有测试更危险,因为它给了你信心。
出向 Mock 的落地方式有两种:起一个假服务、把基址指过去,或者在客户端层注入一个可编程的传输实现。前者更接近真实,连超时和连接错误都能造;后者接入成本低,但测不到网络层。团队小就用后者起步,等到开始排查「明明返回失败却已经生效」这类问题时,再换成前者。
也要明确 Mock 不该覆盖什么:鉴权、网络抖动和平台行为的演进,这三样造出来的都只是你想象中的样子。见过团队把凭证过期也 Mock 了,结果真的过期那天,那段刷新与重试逻辑第一次运行就是在生产上。
Mock 还一定会漂移,它写的是你以为的接口行为。唯一的解法是留一条每天自动跑的真链路冒烟,用真实凭证走一遍最短路径。冒烟挂了先怀疑 Mock 过期,再怀疑业务代码。它和线上的合成探针不是一回事:探针盯的是链路健康,冒烟盯的是契约有没有变,两者的接收人往往都不一样。
录制回放:把联调期的报文变成资产
联调期你会收到大量真实事件,多数团队看完日志就扔了。把它们脱敏后存成夹具库,是这里投入产出比最高的一件事:解析、路由、状态机这三层的回归从此不需要任何外部依赖,也不需要账号,CI 里就能跑。
- 1存原始字节,不要存解析后的对象。解析器本身就是被测对象,存对象等于把它排除在测试之外。
- 2脱敏但保留结构。姓名、手机号、消息正文都要替换,长度和字符类型要保留,否则会掩盖掉边界问题。
- 3回放要能改写时间戳并重新签名。带时间窗校验的回调会拒收半年前的报文,夹具必须支持改时间 —— 别用「测试环境关掉验签」绕过去,那会让验签这段代码永远没被测过。
- 4断言写在副作用上:有没有写库、有没有产生一次出向调用、状态机停在哪个状态。不要断言日志里有没有那行字。
// 示意:报文结构与签名字段以 wecomapi 文档为准,别照抄常量
for (const f of fixtures) {
const body = retime(f.raw); // 改写时间戳,绕开时间窗拒收
await fetch("http://localhost:3000/wecom/callback", {
method: "POST",
headers: signWithTestKey(body), // 重新签名,而不是关掉验签
body, // 原始字节,不要传解析后的对象
});
await expectEffect(f.expect); // 断言副作用,不断言日志
}夹具库会长大,也需要维护。一条经验是按解析层、路由层、业务状态机三档分目录:状态机那一档随需求变动最频繁,允许整体重录;解析层那一档应该长期稳定,一旦需要改就说明契约变了,值得单独看一眼再决定。
夹具进了 CI 还有个隐性收益:新人第一天就能跑通全套回归,不需要账号、不需要密钥、不需要等谁给他配隧道。接这类系统最大的入门成本从来不是看懂文档,而是拿到一个能安全试错的环境。
回放覆盖不了鉴权、网络和平台侧的行为变化,所以它替代不了冒烟:回放守住你自己的逻辑,冒烟守住外部契约。CI 里该放哪几类测试、真实调用摆在什么位置,站内讲多环境管理那篇有完整分工,这里不再展开。
还有一条和工具无关的纪律:非生产环境的出向默认拒绝,只放行固定的测试会话。本地联调最贵的一次事故从来不是服务崩了,而是有人在本地把群发脚本跑通了、而收件人列表指向生产。环境隔离站内另有一篇,这里只强调「默认拒绝」四个字 —— 做成默认放行再加一个 if 判断,等于没做。
三件工具没有强弱之分,缺哪一件就在哪个环节手工补人力,而人力是最先被抽走的资源。示意里的路径与字段只用于说明结构,精确定义以 wecomapi 线上接口文档为准。
常见问题
- 一定要用内网穿透吗,能不能直接在测试服务器上开发?
- 两种都行,取舍在反馈速度。穿透保留了本地断点和热重载;部署到测试服务器每次要打包,但没有隧道那些请求头和超时的怪问题。团队人多、隧道抢不过来时,测试服务器加一层事件扇出反而更稳。
- 回调地址只能配一个,多人怎么开发?
- 别抢配置。在测试环境常驻一个转发服务占住那个唯一地址,由它验签、快速 ACK,再按会话或账号规则把事件复制到各人的隧道。账号数量够时,一人一个测试账号是更彻底的隔离。
- Mock 和真链路怎么分工?
- Mock 负责失败分支(频控、超时、半成功),真链路负责契约。建议每天跑一次冒烟:用 wecomapi 的接口按最短路径发一条消息,再从回调把它收回来,冒烟挂了先怀疑 Mock 过期。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
