两个问题

  • 要解决的问题是什么?
  • 要解决这个问题,是否有更好的方式?

我发现这时不时使用这两个问题可以很好的避免陷入“为了XX而XX”,把手段误当为目的的思维陷阱中。

那么,对于TDD,我们可以问:

TDD需要解决的问题是什么?

假设说:提高代码质量。

那么,提高代码质量是否有更好的手段?当然,TDD也是提高代码质量的手段之一。

TDD的迷思

很多TDD的鼓吹者,会宣称TDD能够帮助我们做更好的设计;我认为:

  • 是的,某些情况下可以
  • 但更多的情况下没有影响
  • 有的情况下,TDD甚至会伤害代码设计

DHH的著名文章TDD is dead核心观点便是有一些程序为了让代码可以被测试,而修改了原有的代码结构以及使用各种mock,这样的行为,会反过来伤害代码设计/质量。

关于这方面的更多讨论,可以参考马丁·福勒的总结

我们应该避免使用mock,能够直接连数据库进行测试,那么就直接连:

  • 不要为了测试而测试
  • 不要为了“方便测试”而去修改代码设计
  • 让设计来适应代码,而不是代码去适应设计

那么,当前的情况究竟是适合还是不适合?

自己判断
自己判断
自己判断

教条
教条
  • 万物皆对象
  • 万物皆资源
  • 必须先写测试再写实现
  • 每个函数都必需带有单元测试
教条
教条

测试案例的实践

在Go语言的实践中,测试编写非常方便;具体到TDD,可以参考LondonGophers 20/03/2019: Dave Cheney - Absolute Unit (Test)的建议:

模块函数实现单元测试Acceptance test–driven development覆盖率

测试覆盖率

覆盖率
gogo test -coverprofile [filename]覆盖率情况
profile

它会记录我们代码中具体行是否已经被测试代码覆盖;亦可以使用网页模式对代码覆盖做更方便的查阅。

go testcoverprofilep.out

打开浏览器来对代码覆盖做浏览:

被测试的代码文件覆盖率,未覆盖的代码一目了然以便我们考虑是否要增加/修改测试案例来覆盖更多的代码。

提醒

100%覆盖
  • 有可能是测试以及实现都错了
  • 有可能测试仍不完备,存在实现没有测试到的场景
100%覆盖
api.Exec
100%覆盖

代码覆盖率虽然上升了,但代码质量实际上却是下降了。

案例

下面我们来考虑一个模拟的案例,以及一个真实的案例;看看在实践中可以如何使用测试,或者说TDD来提高我们的代码质量。

怒气值 FuryCounter

HitFuryBlock
FuryCounterFuryBonusCountBonusLevelHitFuryBonusCountBlock

简单业务

BonusLevel
怒气值
100%覆盖

测试代码中,实际上是编译、启动了整个程序,跑其了web服务器,然后真实调用web接口来进行测试;并且它可以根据被调用的web接口,来判断覆盖率;而且,测试过程非常快,基本是秒完成。

开发实践中,工程师不愿意写测试的一个主要原因是因为测试代码麻烦、运行慢;但这点在Go语言中相对不是问题。

复杂业务

BonusLevel怒气值怒气值Bonus = Fury * BonusLevel

业务瞬间变得复杂起来,在这样的情况下,我就建议先考虑编写测试案例,然后再写实现。

实际上,对于复杂的业务,我相信在确定测试案例时,就可能会发现需求的一些潜在不合理性(真的要支持负值?),那么,就应该立刻跟策划/产品经理进行沟通,以明确业务需求。

对于复杂的业务,要期盼某个人可以一下子就想清楚所有逻辑,是不实际的;实践中,我们更可能需要借用测试案例来帮助我们理清业务,然后再写实现代码。

调用命令参考

购物车

购物车需要支持:

  • 优惠卷
    • 立减
  • 满减
  • 定金
    • 订金膨胀
  • 叠加
  • 加购指定商品

如果我们需要实现这样的购物车,应当如何实践TDD呢?