已经有人提出来,使用单元测试有三种不同方式。一种是传统的方式,先编写代码,再编写单元测试,这种称之单元测试后行;一种是反过来,先编写测试,再编写代码,称之为单元测试先行,即测试驱动开发,英文缩写为TDD;最后一种是在设计整个架构、框架、核心模块时,提前将可测试性也考虑在内,这种称之为可测试性设计。
我使用R语言,结合一些不太严谨的、模拟的但却能反映这三者之间差异的数据,得出了以下这个图。
图1 三种不同的世界
上图中,横坐标表示开发工程师对于不同使用方式的熟悉程度,从0分到10分。很明显,0分表示刚刚开始接触,10分则表示已经是精通或者起码是达到熟悉级别。纵坐标是表示不同使用方式在对应熟练程度上,所能产出的成效。注意哦,有时付出与回报不一定是正比的,也不一定是成线性比率的。成效从0分到10分,0分表示毫无收获,10分则表示作用重大,例如明显提升了项目质量或开发效率。
反过来讲,同一个工具,不同的人来使用,所产生的效果也是不一样的。而这些成效,很大程度上取决于人们使用这些工具的方式和观念。这让我想起一个关于斧头和电锯的小故事。有很久以前,有一个村庄,那里的人主要以伐木为生,使用的工具主要是斧头。有一天,一个外来人向他们推荐了一种可以大大提升生产率的新型工具——电锯。过了一段时间,外来人再回到这个村庄并询问使用电锯后的效果怎样时,那些村民回答说,“好吧,那东西真不好用!比我以前用的斧头重不知多少,而且砍起来比以前费力多了!”。
下面分别来介绍这三个不同的世界。
单元测试后行
为已经编写好的代码补充单元测试,是属于单元测试后行的做法。这种做法,一开始可能会收获一些成果,但随着时间的推移,成本会越来越高,而效果会越来越低。为什么呢?
因为编写单元测试的职责更多在于开发工程师,如果代码已经开发完成并且上线的话,再来补充单元测试,对于他们来说意义就不重要的。代码都已经发布上线,我还编写单元测试做什么?这不是浪费我的时间吗?而且我的代码明明没问题啊,在生产环境运行提不是好好的吗?所以,在他们看来,是多此一举。
确实情况也是这样,如果只是盲目地打着单元测试的旗号强制要求编写单元测试,尤其是采用单元测试后行的方式,就会受到开发工程师心理上的阻碍,难以推行,并且效果很低。实际上,有不少团队是这样做的,但最后都维持不了多久。
这种方式不是我们讨论的重点,暂时先简单介绍到这里。
测试驱动开发
再来看第二种使用方式——测试驱动开发。关于TDD的书籍,已经有很多了,例如Lasse Koskela编写的《有效的单元测试》、Kent Beck编写的《测试驱动开发:实战与模式解析》等。这些经典的书籍经常会在讨论单元测试的书籍中补引用,也是我收藏的书籍之一,建议大家有空也可以阅读一下,大有裨益。
测试驱动开发,是在编写代码前先写测试。这一点,说起简单,做起来很难。因为多年来我们已经习惯于直接就编写代码,不管是最初学习编程时,还是以前做项目过程中,抑或是平时和别人沟通交流时。从根深蒂固的传统思维方式切换到意图导向编程,确实需要有意识地坚持,并挣扎好长一段时间。这就好比如从面向对象编程范式切换到函数式编程范式一样。
那什么又是意图导向编程呢?这是为了让大家更好、更容易地理解测试驱动开发,有人提炼了此概念。意图导向编程,提倡的是始终高度保持关注点,并且有且只有一个关注点。这可以使得开发人员在同一时间专注做一件事情,这也是我们编程的艺术之一,最终让程序员能抛出其他的杂念,进入一种忘我的、快速高效的流状态。此外,即便被外界打断,例如开会、聊天回复、上洗手间、临时讨论、查看邮件,也能通过意图导向编程快速切换上下文,快速回到流状态。
图2 意图导向编程
测试驱动开发体现的哲学是:做正确的事,比把事情做正确更重要。意图导向编程则是坚持把事情做正确。理想状态,当开发新一个需求时,最短路径是两者之间。但实际上,很难轻易实现,例如有用户注册时要检测账号是否已注册、账号是否含有非法敏感的词语、密码长度和组合是否满足安全性要求等。糟糕的是,在开发过程中, 大多时候会被外界打断,同时还要并行处理很多事务,繁忙来回切换工作的上下文,就会造成混乱。混乱滋生异味代码,引入熵,最终加重了维护成本。
在测试驱动开发下,遵循“红-绿-重构”这样的流程,我们可以在更高的层面关注需要实现的功能需求,并自顶而下地进行设计优化,编写精益的代码。而在意图导向编程的帮助下,让我们有了在夜里航行的指导,不断修正偏移的方向,以最快的速度驶向终点。
这种方式使用恰当并熟练,产生的价值是非常令人满意的。这也是本章讨论的重点,下面会继续深入讲解。
可测试性设计
最后一种使用方式是可测试性设计。当使用测试驱动开发很长时间后,你就会发现它的瓶颈所在了,因为它重在关注怎么在某一个点在做正确的事情。那更大的范畴呢?例如,整个系统的设计、核心的执行流程、底层模块的集成,这些更大的层面又应该怎么与单元测试关联起来?
图3 在设计计划任务系统时提前考虑可测试性设计
虽然敏感开发、极限编程都建议不要过度设计,但不要误会,不是完全不需要设计。对于有一定复杂度、有一定规模的系统,提前考虑核心的设计是有必要的。这些核心的设计中,如果能将可测试性纳入在内就更完美了。例如在设计一个计划任务系统时,假设采用的是桥接模式作为主模式来实现,那么针对客户端调用、数据的划分以及行为的实现分别进行测试,就能在关键的环节对此系统进行充分的测试和验证。从而形成配套的测试套件,最后换来的收益是系统的稳定性、健壮性,而不是在毫无测试或者难以测试的情况下直接发布上线,出现线上故障后再花上好几天的时间来逐步排查。可测试性与可维护性,就像一对孪生兄弟,拥有其中一个,就能体现另一个。