muxin space

这次重构的起因,说出来你可能不信——是一个测试用例怎么都跑不通。

不是代码写错了,是流程的时序写反了。

旧设计的问题:人只能在”事后”确认

以前的测试引擎是这样工作的:

改前流程图

API 调用 → 弹出确认卡片 → 人工确认 → 自动断言。这个顺序看起来很自然,直到遇到了两种反直觉的场景。

场景一:扫码测试

扫码之前,人需要先把二维码放在摄像头前。但旧设计里,人只有在 API 调用完成之后才会看到提示。于是:

API 已经开始扫了 → 人才看到”请放置二维码” → 二维码还在桌上

这就好比让你先闭眼再找眼镜——顺序反了。

场景二:按钮点击测试

有些 API 调了之后会进入监听状态,等着人来点击按钮触发回调。但旧设计里,确认卡片要等 API 完成才弹出来,API 又要等点击了按钮才算完成。

API 等按钮点击 → 确认卡片等 API 完成 → 人等看到卡片才去点 → 按钮没人点

一个教科书级的死锁。

这两个问题让我意识到一件很底层的事:人工操作不是 API 调用的”后置动作”,它可能发生在调用的任何阶段。

把这两个场景放到同一条时间线上看,会发现它们其实在争奠两个完全不同的位置:

同一条时间线,两种场景各自的位置

扫码需要的是”调用前”那一个位置,按钮测试需要的是”调用中”那个位置——它们之前都在报错,不是因为逻辑写错了,是因为这个位置根本不存在,只能强行把两种完全不同的需求塔在同一个”调用后”的位置上,难怪一个卡住、一个死锁。

不是推翻重来,是在时间线上补位

想清楚了就好办了。人工操作其实分布在三个阶段:

  1. 调用之前——准备环境、放好物体
  2. 调用之中——在回调监听期间做交互
  3. 调用之后——确认最终结果对不对

改后流程图

中间那一环是关键:人和回调监听同时运行,人在操作,监听在收集数据,互不阻塞。

而之前只有一个”调用之后”的位置,前两个阶段的需求没地方放。加了两个新位置之后,死锁自然就解了——不是什么精妙的算法,只是给了正确的时序。

同一场景,改前 vs 改后

对比图

扫码从”先扫再放”变成”先放再扫”——不是技术变了,是流程的时序摆正了。

按钮测试从”死锁等不到”变成”中间插一步交互”——解决了 callback 型 API 天然需要人在中间操作的问题。

这次我想通的三件事

1. 设计的大多数问题,长得都像”功能不够”

旧设计只有一个 manual 字段,直觉反应是”那再加一个呗”。但真想清楚之后发现:不是加一个的问题,是加在哪个位置的问题。

问题的本质不是”一个字段不够用”,而是”你只有一个时间点可选”。当你把时间线展开,自然就有三个独立的位置——前置、中置、后置。每个位置有自己的职责,有自己的失败处理逻辑,互不干扰。

感悟:当你觉得”再加一个功能就好了”,停下来想想——是不是模型本身缺了一个维度。

2. 向后兼容不是口号,是设计水平

这次改动的底线是:现有所有用例零修改就能跑。

怎么做到的?很简单——新字段缺省不触发。preManual 不写就是 null,midManual 不写就是 null,引擎跳过对应逻辑,行为完全不变。老字段 manual 的语义一行不改。

这不是什么高深的工程技巧,但做到不容易。因为”加功能”通常会忍不住”顺便改一下老逻辑”。控制住这个冲动,把新东西放在独立的新路径上,才是真正的克制。

感悟:好设计不是在旧代码上堆新功能,而是新功能有自己的路径,碰不到旧逻辑。

3. 死锁不需要高级解法,需要换个视角

按钮点击的死锁问题,如果从”怎么让两个异步操作不互相等待”去解,很容易掉进 Promise 编排的坑里。

但换个视角看:死锁的根源不是异步问题,是时序假设错了。你假设”人工操作一定在 API 调用之后”,但 callback 型 API 天生需要人在调用期间做操作。修正这个假设,加一个”调用中”的位置,死锁就没了。

感悟:有些问题看起来是技术问题,其实是你对世界模型的假设出错了。修假设比修代码重要。

整条链路都要带上,否则就是半吊子

一个字段从设计到最后能用,中间要过很多关。

Schema 文档要更新,类型定义要加,引擎执行逻辑要改,校验规则要补,过滤器和目录要扩展,UI 上的交互卡片要按阶段区分标题和提示文案。

改了十几个文件,四百多个现有测试全部通过。

能全部通过,关键不在于注意,在于新字段遵循了一个简单到几乎不值一提的原则:不声明就彻底不介入。

新字段默认不触发,老用例一行不用改

这张图其实就是整个向后兼容方案的全部:旧用例不写新字段,引擎看到未声明就直接跳过新逻辑,行为和改动前一比一一样;新用例按需声明,引擎才会在对应位置多做那一步。两种用例并存在同一个引擎里,谁也不影响谁。

这里最大的感受是:系统里的一个小改动,往往会在你不注意的地方有连锁反应。 如果你只改了核心逻辑就跑,那些”不注意的地方”迟早会炸。花时间把整条链路走完,不是为了完美主义,是为了别给自己埋坑。

没做完的部分

用例迁移还没搞完。几个典型的 callback 型 API 用例还需要从旧写法迁到新写法。但引擎和 Schema 的底子已经搭好了,后面就是收敛的工作。

有意思的是,当我把人工介入拆成三个阶段之后,UI 上的交互提示自然变得更清晰了——因为是按阶段分的,每个阶段该说什么一目了然。这是写设计文档时没预料到的额外收获。


一句话总结:人工介入不是一个动作,而是一条时间线。把时间线理清楚,比加多少功能都管用。