大白话解释为什么单元测试在正规项目里十分重要

Written on February 17, 2016 View on GitHub

在过去的几年里,遇到过一类人。他们写代码速度快,但是小错误百出;他们崇拜大牛(比如发明了某种语言、框架、软件的人)但又鄙视经验丰富的前辈;他们总是能把产品的各种问题归结到队友头上;他们有强烈的价值观输出倾向;讨厌写测试。

这类人在程序员届并不少见,相信每个人都多少见到一两个。除了中二之外,我能很明显能看出来他们的一些知识面的缺陷。(比如他们之所以崇拜大牛,是因为不了解发明一种某种语言、框架、软件,到底需要什么样的知识和工作量。之所以要说服你,是因为他们盲目的认为他们观点是正确的。)

关于测试,他们不认为测试是开发产出的一部分,甚至我见到有坚信写测试没有好处的人。

好在大部分程序员都是认可测试的作用,但是没有足够经验去最大化这样的作用。这里我稍作总结一下测试的作用,以便直观地感受什么情况下需要写测试。

好处

明确功能

这简直不能算是“好处”,这是一个交付前提。

我们来想象这么一个场景:

“你的代码有问题,我已经修复了。”
“什么问题?”(但心中想:“你是不是搞错了。”)

越是没有经验的人越会武断地认为别人的代码有问题,而经验丰富的程序员则应该对自己的代码有一定信心。这样的对话并不少见。而实际情况则很有可能是:

  1. 代码有问题,修复了。
  2. 搞错了,代码没有问题,问题在别的地方。“修复”事实上修改了功能。
  3. 代码有问题,“修复”也有问题。

个人经验上述几种情况出现概率相当,所以这个所谓的“修复”很有可能会让情况变更糟更复杂。但这并不能单方面认为是“修复”者的责任,这样的事情能发生,就说明这部分代码没有很好的被测试覆盖。

如果这么代码有功能测试,则可以:

  1. 证明代码是没有问题的,或者说至少证明在部分情况下是没有问题的。
  2. 如果“修复”破坏了原有功能,则会导致测试失败,“修复”的作者能意识到这里的问题。
  3. 如果“修复”作者修复了问题,那么他可以添加一些测试,说明之前的代码是有问题的,自己的“修复”能解决问题。

现在很多软件开发的教学已经提到 Contract 的概念。换句话呀说,每段代码都应该说明和证明自己的 Contract。测试则是证明 Contract 的主要手段。

明确功能(大白话版)

你丫写给代码敢说没问题的,不写个测试你怎么证明代码就没问题了呢。你现在没问题,改着改着改出问题咋办? 写一个 UT,各种加 Assert,比眼睛看要准。

阻止 BUG 再次发生

BUG 的发生肯定是有地方比较迷惑,既然比较迷惑那就很有可能在此发生。 发现了一个 BUG,修复之后,加一个 UT。以后只要保持每次提交代码,UT 都没有被 break,就保证了这个 BUG 没有被复现。

阻止崩溃

程序员有很多经典的笑话,讲的是不要去碰年代久远系统。本想尝试去修复一个 BUG,或者做一个改进,然后世界崩塌了。 有足够多的 UT,并且及时做 Regression Testing,相信他的改动也不是那么灾难性的。

学习代码参考

Contract 概念的一部分。 维护文档本身是麻烦的,尤其是独立的文档,很容易过时。 一个常见的思路就是注释当文档。但注释文档往往只能解释代码本身,如果对不同的数据都一一做文档,注释会很长。 所以另一个办法就是写 UT,然后每个 UT 可以加一个叫解释:“输入是这个的时候,输出是这样的”。 很多开源项目拒绝提供详细的非功能性的文档,因为维护成本极高。

监控环境变化

Contract 概念的一部分。 实际编程时候,会有很多假设,特别是在使用第三方库的时候。 比如:假设“这个函数不会返回空”,尽管你编程的时候是这样的,但几个月更新的时候,就不一定还是那样了。 写几个针对第三方的库的 UT,某一天这个 UT break 的时候,就提醒你有可能有潜在 BUG 了。

对松耦合的保证。

大家都知道松耦合的优势。但很多时候,很难把握。 写 UT 的时候,如果耦合度太高,一大表现就是 UT 无法写,或者说 UT 之间会互相影响。 这实际上是一个提醒,你写的东西是否过于耦合。

弊端以及如何回避

拖慢进度

这是当然的。但从项目整体上来说,是有利的。 在微软我们经常允许短时间内的 UT 缺失,特别是一些新模块。(新加一个模块,允许几天后甚至几周后再加 UT) 微软的 Engineer Manager 会吧测试作为开发的一部分看待,但并不是每一个公司都这样。如果你的老板不这样看,那就随便玩吧。

环境依赖

实际程序可能会在一个特殊的环境下允许的,比如特殊的网络,特殊的 Run Time 之类。 一般的做法是把这些特殊的东西用一个 Interface 包装起来,UT 的时候使用一个 Mock(Fake) 的 Interface。或者另一个思路,你的代码愿意接收一个插件式的 Interface,然后再 UT 的时候 Hook 进一个特制的实现。 这些 Mock/Fake/Hook 通常并不完全准确,但好在是可以复用的。