4.1 测试先行如何浮现设计


4.1 测试先行如何浮现设计

本节摘要:"先写测试"常被当成质量手段,它更深的作用是设计手段:测试是代码的第一个使用者,使用者的较真会逼出松耦合的接口与可替换的内部结构。本节拆解这条因果链,给出"测试疼点即设计卡点"的诊断法,并演示一次从红灯到新结构的三次挤压。读完你应能主动利用测试先行为设计施加压力,而不是被动等设计变好。

别以为设计是天才画完蓝图、程序员照图施工的产物——在需求按月变的项目里,一次画完的设计三个月后必然走样,问题从来不是"设计得不够远见",而是"设计缺乏随需求进化的机制"。TDD 给的就是机制:每条新红灯都是一次小型设计评审,逼你在最小范围内重新审视结构。设计不是被想出来的,是被循环一格一格挤出来的。

第一个使用者的较真

接口好不好用,写实现的人说了不算——他太了解内部了,怎么用都顺。测试不一样:它是第一个只看外部、不看内部的使用者。这个身份带来三重较真:

  1. 它要求接口可达。测试要在干净的上下文里构造被测对象——如果构造一个 Checkout 必须连带搭起数据库和真网关,测试就会逼你承认:依赖该注入,而不是内建。第 3 章的替身功夫在这里兑现:能替,说明依赖是显式的、可替换的。
  2. 它要求行为可描述。断言写不出来,多半是接口职责含混。写 rate("aB1") 返回什么时你得想清楚"拒绝"和"原因"要不要分开交付——这一想,Rating 数据类就长出来了。
  3. 它要求上下文可复原。用例跑完世界得回到原样,逼你把全局状态改成语义化的对象边界。反面教材就是 3.1 那个 set_global_promo——测试写不下去,正是设计在报警。

三重较真合起来,就是"可测试性"的实质:可测试的接口几乎总是松耦合的接口,因为测试无法进入内部较真,只能从边界提要求。

测试疼在哪,设计就卡在哪

反过来用更妙:测试的疼点是设计卡点的探测器。积累了一个版本周期的实战对照表如下:

测试的疼点 背后的设计卡点 重构方向
构造被测对象要连带搭一堆陪葬品 构造函数职责过重 依赖改注入,陪葬品换成替身
断言要深入对象内部翻字段 职责泄漏,状态外翻 补行为方法,让内部保持私有
一条业务规则散在三个测试里 规则没有归属,逻辑游离 收拢到一个领域对象
mock 链一路打点穿三层 违反得墨忒耳定律,穿透陌生人 在中间层补一个讲人话的方法
每加功能要改十条测试 测试锁了实现细节 断言从结构词改行为词

这张表值得贴在工位上——它把"测试代码坏味道"直译成"设计坏味道",用测试当探针,比定期重构评审便宜得多也及时得多。

从红灯到新结构:三次挤压

看灯塔小组的实际案例。新需求:"黑名单用户的订单不能使用任何优惠券。"第一条红灯:

def test_blacklisted_user_gets_no_coupon_discount(): pricing = Pricing(blacklist=InMemoryBlacklist(["u-666"])) order = an_order().by("u-666").with_coupon("SAVE20").build() assert pricing.payable(order) == order.raw_total() # 券不计入

写这条测试时较真立刻发生:Pricing 现在需要"查黑名单"的能力——测试逼你决定黑名单从哪来。注入(如上),还是让 Pricing 自己去查库?前者测试好写,后者测试要陪葬数据库。选择注入,第一次挤压完成:黑名单成为显式依赖

第二次挤压来自断言:payable 这个名字是写断言时才确定的——原来函数一直叫 total,语义含混"折前折后";要断言"黑名单不享受券",接口必须能表达"应付"这个概念。红灯逼出了更准的词汇。

第三次挤压在重构窗口:黑名单判断与券计算搅在同一个方法里,下一轮"白名单加倍"需求必然冲突。提取 _apply_coupons 时把黑名单检查挪到券逻辑上游,策略结构(第 4.2 节的主角)顺势接管。三次挤压,没有任何一次坐在白板前"做设计",结构却在每个红灯的推动下挪到了它该在的位置。三次挤压的方向也值得注意:没有一次是"加抽象",全是"挪位置"——浮现式设计的动作通常是移动和命名,而不是发明新概念,这也是它比想象中便宜的原因。

图:测试先行浮现设计推演图

图:测试先行浮现设计推演图

⚠️ 别把"测试驱动设计"理解成"测试决定设计"。测试提供的是压力与时机,取舍仍要人做——黑名单注入还是内查,测试只负责让两个选项的成本显形,拍板的永远是你。

换个需求再验一遍

机制讲完,值得换个完全不同的需求复验因果链是否成立。新需求:"日报要在每天早上八点推送到值班群。"红灯逼出的东西依次是:推送目标得是可注入的通道协议(不然测试要真发消息)——较真一;should_send_now 之类的判断得能被假时钟驱动(5.3 的伏笔在此接上)——较真二;"日报"与"推送"被分成两个对象,因为断言要求能单独验证内容生成而不碰通道——较真三。三个不同的需求,浮出来的却是同一批结构类型:注入的协作方、讲业务话的接口、单一职责的边界。这不是巧合,是"第一个使用者"较真角度的必然产物,也是你可以在自己项目里反复验证的预言。

💡 自查一条:回顾你最近一个"测试很难写"的模块,按上面对照表找疼点对应的卡点。十有八九,你会在改完设计的当天发现测试变好写了——这个正反馈就是本章全部内容的体感版。

本节要点回顾

  • 测试是第一个使用者,三重较真(可达、可描述、可复原)逼出松耦合。
  • 可测试性与好接口高度重合,为测试设计就是为使用者设计。
  • 测试疼点是设计卡点的探测器:构造重、断言翻内部、mock 链长,各有对应重构方向。
  • 一次设计演化可以拆成多次挤压,每次由一条红灯推动,白板会议可以省了。
  • 浮现式设计的动作多是移动与命名,不是发明新概念——它比想象中便宜。
  • 下一节把"模式如何浮现"落成可复现的演练:策略、模板方法、观察者各来一遍。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U