测试驱动开发真的有用吗?三年实践后,我和传统测试做了个对比

🔑 关键词:测试驱动开发,TDD,单元测试,重构,代码质量

📖 摘要:实践过三年TDD后,我发现它的优势不在测试本身。本文用个人经历对比传统测试和TDD,说说为什么有人把它当设计工具,以及哪些场景别硬用。

先说个囧事。2019年我加入一家外包公司,上面要求所有项目必须TDD。我当时刚毕业,觉得这就是形式主义,每天先写测试再写代码,效率低一半。更糟的是,我写的测试自己都看不下去——为了凑覆盖率,连setter都写断言。直到有一次,我花三天修一个支付回调的bug,只改了几行逻辑,可每次改完都感觉哪哪儿都可能炸,像在雷区里走路。后来我实在忍不了,把那个模块抽出来,顺手补了一套单元测试——那不算TDD,是后补的。结果特别讽刺:这些后补的测试全都通过了,但代码里的bug还在。为什么?因为补测试的人是我,我会不自觉地照着已经写好的实现去构造输入和期望,等于自己证明自己没错。

图片

后来我开始真正尝试TDD,才知道什么叫被“先写一个红”逼到墙角。对比后置测试,TDD里最烦的一步不是写代码,而是写一个会失败的测试。这个红不是找茬,它逼着你先回答一个问题:这段代码的输入什么、输出什么?还没动手就把接口定下来,这事儿比写实现难多了。以前我写代码喜欢先写实现,然后发现缺什么参数再补一个,最后接口丑得连亲爹都不认识。TDD逼着我在测试里先当一个调用方,比如做积分兑换时,我不得不想清楚该如何调用:exchange.canExchange(points, goodsPrice) 返回布尔值?还是抛异常?等我把断言写出来,方法签名已经跑不掉了。后来业务要区分“积分不够”和“库存不足”,那个测试又逼我把canExchange拆成更细的方法。所以我说,TDD更像是接口设计的磨刀石,只不过它顺便产出了测试用例。

图片

当然我不会无脑吹TDD。在UI层和配置表这类需求上,TDD几乎让我崩溃。按钮闪一下、弹窗多一条文案,这种破需求我根本没法先写断言,因为元素可能明天就换。我曾经试过为一个页面的联动逻辑硬上TDD,结果测试写了三个小时,页面改了两行,测试就红了一片。后来这些测试全部删掉,浪费的时间又变成我的血泪史。于是我自己总结出一条规律:TDD只对“确定性输入输出”的领域逻辑友好,对“频繁视觉/交互变化”的地方就是死局。核心业务能用,边缘UI别碰。真有人能搞定UI层的TDD?我佩服,但我做不来。

图片

真正让我坚定怀疑被扭转的是去年初的退款规则模块。那个模块里有七种退款路径,判定条件互相干扰,还牵扯到优惠券、积分、部分退款等情况。要是按以前的习惯,我肯定先写一个巨大的函数,然后再补if…else…。但那次我压住性子,先从所有已知的退款场景里列出12个预期结果,把它们写成一个一个失败的测试,每个测试都是在描述业务/条件X/结果Y。等到12个测试全部跑红,我对这个模块的设计已经比原文档还清楚了。接着我开始逐个实现,差不多一个下午就全绿了。更精彩的是,在我准备收工的那一刻,我突然发现有一个场景没有测试覆盖:当订单已经实施部分退款后再整单退款,退款金额必须减去已退部分。原来代码里没有这个逻辑,它一直是个隐藏bug。这个bug在旧系统里躺了至少两年——如果我先写实现后补测试,我的测试只会围着实现转,根本想不起这个场景。只有在“先列行为,再写代码”的逼问下,你才会认真把自己当产品经理挨个追问边界。

图片

现在我敢说一句可能有点叛逆的话:TDD的测试用例,本质上一个团队对“业务契约”的共识清单。它的价值不在于告诉别人代码怎么工作,而在于把所有能听到的“如果……那……”翻译成可执行的句子。我到现在也没做到每次都TDD,赶工时照样先撸代码。但只要涉及规则复杂、容易被因果链坑死的模块,我会在键盘前先停下来。原来我的第一反应是“怎么建表、写函数”,现在变成了“有哪些输入输出能被写成测试”。这种思维上的偏移,比什么红绿循环提神得多。

图片

🏷️ 标签: