测试驱动开发(TDD)到底怎么用?一个后端程序员三年的实战总结

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

📖 摘要:本文从一个使用TDD三年的后端程序员视角,聊聊真正落地时踩过的坑、必要的套路,以及什么时候该放弃。内容包含具体步骤、JUnit 5示例和覆盖率数据。

先说说背景吧。2019年春天,我在维护一个订单分账系统。每次改接口都害怕,总有用户反馈金额不对。搞了两次线上补偿后,我决定重写核心模块。我当时强制自己用TDD,从最简单的calculateFee()开始。头两周真的受不了,得先写一个会失败的测试,看着它报红,再去写实现,感觉纯粹是多绕一圈。但到了第三周,产品经理又提了个需求,要增加一个阶梯费率。我只改了业务规则,然后跑了一遍已有的17个测试,全部通过,那种踏实感是之前从没体验过的。

图片

具体步骤其实很简单,下面是我一直用的套路。我用的测试框架是JUnit 5.7.1,断言库用AssertJ 3.19.0,覆盖率统计用JaCoCo。第一步,红:写一个期望的断言,比如Fee fee = FeeCalculator.calculate(orderAmount, channelRate); assertThat(fee.value()).isEqualByComparingTo("0.30"),这个0.30是我提前用手算好的。然后执行mvn test,看到它失败,必须是失败,不是报编译错误。第二步,绿:写最少量的代码让测试通过,别想着一口气把整个分账逻辑写完。第三步,蓝:重构,把数字0.30变成常量,或者把if-else换成策略表。这里最容易栽的坑是重构的时候连测试一起重写了,导致行为偷偷变了。我的经验是:测试只针对外部行为,比如输入和输出,不要断言内部私有方法。

图片

当然,并不是所有代码都适合硬套TDD。我后来遇到一堆Controller和Repository,mock来mock去,代码又长又脆,一个小改动就碎一片。我的折中方案是:只对核心业务Service层执行TDD,覆盖率用JaCoCo卡到90%,其他层不看。另外,别为了100%覆盖率去写assertNotNull(someList)这种没营养的断言。因为有一次我为了凑行覆盖,给一个缓存工具类补了个断言,后来缓存返回null,测试照样通过,但它根本没验证到点子上。那次之后我定了团队红线:覆盖率可以低,但每个断言必须先证明它能失败。

图片

最后说说我的真实观点。TDD本质上不是一种测试技巧,而是一种设计工具。它逼着你在写逻辑前思考依赖怎么抽、参数怎么传,所以代码自然变得好测。还有,如果项目刚开始、需求像雾里看花,或者是一次性的运维脚本,就别用TDD,那确实浪费时间。但如果是核心模块,且要维护超过三个迭代,早点用TDD是划算的。我的账本:之前没有TDD的时候,每个迭代平均要修3个线上bug;用TDD半年后,这个数字降到了0到1个,而且剩下的bug通常还是数据库并发导致的。这就是我坚持用它的理由,但我也尊重有人不喜欢这种节奏。

图片

🏷️ 标签: