所有代码都需要单元测试覆盖吗?

test-coverage

单元测试(unit testing)已经越来越得到广大开发者的认可。作为低成本、速度快、稳定度高的自动化测试手段,单元测试可以在类和函数级别对代码进行质量守护,有助于避免尴尬、耗时的错误。当然,相比功能测试(Functional testing)和端到端测试(end-to-end testing),单元测试能够寄予的产品级别的信心要略低一些,因而各个粒度的测试应该是相辅相成的,互为补充。

常常听到一些组织要求开发团队提高单元测试覆盖率,换来的却是怨声载道,或者是一堆应付差事的垃圾测试(没有断言的测试,都见过吧)。尽管,低测试覆盖率意味着对质量的信心不足,但是,单元测试覆盖率真的要达到100%才好吗?

100%听起来肯定比95%要好,但是区别在于那些额外测试的价值对你可能是微不足道的。这要看哪种代码没有被测试覆盖,以及你的测试能否暴露程序的错误。100%的覆盖率并不能够确保没有缺陷——它只能保证你所有的代码都执行了,而不管程序的行为是否满足要求。与其追求代码覆盖率,不如将重点关注在确保写出有意义的测试。

测试越多,额外测试的价值越少。第一个测试最有可能是针对代码最重要的区域,因此带来高价值与高风险。当我们为几乎所有事情编写测试后,那些仍然没有测试覆盖的地方很可能是最不重要和最不可能破坏的。

编写一些测试是不费脑筋的,但随着我们接近完全的代码覆盖率,我们不那么确定了——我们差不多已经为一切都编写了测试,而剩下的没有测试的代码是微不足道,几乎不会破坏。这就是所谓的收益递减。要想从单元测试中获得更多的收益,需要重新将单元测试从质量工具定位成设计工具。TDD等方式可以帮助我们做到这一点,(关于此话题,敬请期待@申导 翻译的《Effective Unit Testing》一书。)

b

换句话说,向代码基增加100个精挑细选的自动化测试是明显的改善,但当我们已有30000个测试时,这些额外的100个测试就无足轻重了。

比赛场上的赢家是那些将注意力集中在赛场上的选手,而不是紧盯着计分板的人。—巴菲特
总之,当你觉得生产环境中报来的bug很少了,或者你能够自信地对代码随时进行修改,单元测试就已经足够多了。

另一方面,也有些观点认为,不但不值得追求高覆盖率,甚至写单元测试本身就是非常耗时和难以维护的重复工作。这种极端观点我同样不赞同。

代码覆盖率不能告诉我们代码质量的高低,也不能用了评判开发人员的绩效。但它能够告诉我们哪些代码还没有被测试覆盖,哪里有漏网之鱼。至于这鱼值不值得抓,还是取决于开发人员的经验进行风险判断。那么,接下来我们再来分析一下,到底哪些鱼值得抓。

unit-test-quadrants

图中,产品代码可以分为四个类别,纵轴是从单元测试中得到的收益,横轴是单元测试的成本,我们从投入产出的角度来分析,到底哪些代码适合于进行单元测试:

  1. 琐碎且无甚依赖的代码。比如getter/setter, 比如简单地调用系统时间,比如 toString()等等,基本是不需要测试的。虽然测起来容易,但我们有信心说它们出错的概率也非常低,测这种代码的确索然无味。
  2. 承上启下的代码。比如用MVC框架实现的代码里,某些service层只是简单地被Action层调用,然后转发到下一层去。这种粘合代码不具备太多被测试的价值,而且由于是衔接上下两层的传话筒,测起来却需要对周围各层进行mock或打桩。要想验证其所做的那一点点工作,其实还挺麻烦的。当然,也有一些观点比如”London School TDD”坚持认为,对于企业级应用,就要像制作香肠一样,一层一层地对交互(interaction-based)进行测试,每测一层都需要mock的帮助。
  3. 具有算法和业务逻辑的代码。比如排序或处理数据等代码,这些是最值得进行单元测试的代码了。虽然有一定的成本,但是由于算法逻辑的输入输出非常确定,结构复杂且具有业务价值,外部依赖较少,在这上面投入是一定可以得到丰厚回报的。这即是”Classic TDD”观点,对状态进行测试(state-based)。
  4. 过于复杂的代码。充满复杂逻辑、交织在一起、散发着各种坏味道的遗留代码,就像一座未开发的金矿,你需要做很多清理工作,才能顺利地进行开采,否则将寸步难行,不知从何下手,而且成本极高。这种代码建议先进行粗粒度重构和接口测试(孰先孰后要根据实际情况),破除掉严重的依赖,找到所谓的接缝(Seam),将具有逻辑的部分与承上启下的代码分离开,然后立即将单元测试注入到接缝中,形成对有逻辑代码的保护网,随后继续缩小重构的范围,不断地添加测试和重构,逐渐渗透形成一张网,将又臭又硬的代码块切碎。
    除了3)以外的代码怎么测?可以采用其他测试手段,比如功能性测试来进行粗粒度覆盖,同样能提升对产品的信心,并且成本更低,特别适合敏捷迭代式开发,能够在有限的时间盒内达到预期的质量水平。

此外,持续地重构代码,同时编写更有效的单元测试,也是敏捷开发者应该具备的基本功,有助于提高投入产出比。

参考:

http://martinfowler.com/bliki/TestCoverage.html

http://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/