第一章 为什么使用单元测试 1.1 程序员的工作——修改软件
修改既有代码是程序员谋生的手段。但是为什么我们需要去修改软件呢?修改软件有以下4个主要起因:
● 修正bug
● 添加新特性(feature)
● 改善设计
● 优化资源使用
这4项都与软件的“行为”密切相关,见下表。
| 软件的既有行为 | 软件的新行为 |
修正bug | 改变软件的既有行为 | 增加新行为 |
添加新特性 | 保持软件的既有行为,完全不修改既有代码 | 无 |
改善设计 | 保持软件的既有行为,但软件的可维护性得到提升 | 无 |
优化资源使用 | 保持软件的既有行为,但软件的性能得到提升 | 无 |
通过这张表格我们看出:只有在修正bug时,我们才需要改变软件的既有行为,而在其他情况下,我们都需要保持住软件的既有行为。如果我们在改善设计,优化,或添加新特性时改变了软件的既有行为,那我们实际上是给软件引入了bug。
可是程序员的工作就是修改软件,所以我们有很多的“机会”给软件引入bug。有什么办法能让我们的生活轻松一点,而不用因为修改代码引入bug而担惊受怕吗?
1.2 软件夹钳——测试
我们已经看到:在大多数情况下,我们希望对软件所做的改动不会改变系统的既有行为。即使是对于修正bug这种情况,我们也希望一旦bug被修正,那么修正后的正确行为能够得到保持,而不会被再之后的代码修改所改变。怎样做到这一点呢?
让我们这样想想:如果我们能在对代码进行改动之前,用一种“软件夹钳”(software vise)来固定住软件的既有行为,那么我们就可以放心大胆地去修改代码了。那么,又是什么可以来充当“软件夹钳”呢?答案是:测试。我们可以这样想一 下:当一段代码被一组良好的测试所覆盖时,我们就可以放手去修改这段代码,并在修改完成之后立即运行这组测试,来验证我们的修改并没有改变既有行为而引入 bug。如果确实改变了既有行为,那么测试就会明确无误地发出警报。由此可见,测试就是程序员所需要的“软件夹钳”。
1.3 单元测试与集成测试之争
我们已经知道了“测试”就是程序员所需要的“软件夹钳”。但是测试分为单元测试和集成测试,程序员需要哪种测试呢?让我们来分析一下程序员需要什么样的测试,这样或许我们就能知道程序员应该更偏向于哪种测试了。
● 程序员需要的测试应该是能帮助程序员定位错误的,这样程序员才会真正地从测试中得到“实惠”。
● 程序员需要的测试应该是很容易执行的,最好是只需点击一个按钮或键入一条命令,这些测试就能运行,而无需费时费力地去搭建测试环境及准备测试数据或仪器。
● 程序员需要的测试应该是运行速度很快的,最好能在几分钟内完成,这样程序员才能快速地得到反馈,采取下一步动作。
● 程序员需要的测试应该是易于写就的,而不愿意对代码基大动干戈,这样程序员才会愿意去写这些测试。
● 程序员需要的测试应该是自动化的,可重复的。这样程序员才会愿意重复多次地去运行这种测试。
比对程序员的需求,我们可以发现集成测试往往不能满足程序员的这些需求:
● 集成测试由于涉及多个模块,因此往往不能提供准确的错误定位信息。
● 集成测试的执行时间一般较长(小时级),这不能给程序员带来快速反馈。
● 集成测试可以自动化执行,但前提是把测试环境和测试数据等事先准备好。
● 集成测试由于不太容易写就,通常不是由程序员写就,而由专门的测试人员写就。
相反,单元测试,尤其是良好的单元测试,恰恰正是程序员所需要的那种测试:
单元测试针对单个类或单个方法,能很有成效地帮助程序员准确定位问题所在。
单元测试应该是执行时间很短的,全部执行也只需5到10分钟,程序员正好可以去喝喝咖啡。
单元测试是一种“虚拟”测试,重在测试代码逻辑,因此一般不需要真实测试环境和测试数据的支持。
单元测试是很容易写就的,尤其是有单元测试框架的帮助时。
由此可见,对于程序员而言,需要的是单元测试。程序员使用单元测试来充当软件夹钳,并在修改代码时获得快速反馈,从而更有信心地投入到修改软件的工作中。
1.4 进行单元测试的其他好处
我们已经知道了单元测试带来的一个好处:它可以充当程序员的“软件夹钳”,在程序员修改软件的过程中给予程序员快速的反馈,帮助程序员定位问题,避免引入bug。单元测试就只有这一个好处吗?不是的。下面我们就来看看引入单元测试带来的其他好处。
1.4.1 单元测试是代码的“活文档”
让文档及时反映软件设计和代码的最新情况,这是一个颇有挑战性的问题。一种较好的思路是:使用“内部”文档,即把文档同代码“拴”在一起。这样当代码发生改变的时候,文档也能相应更新。注释就是一种内部文档,良好的注释应该反映代码的最新状况。
类比来看,单元测试同样也是内部文档,因为单元测试本质上也是描述了被测类或被测方法的行为。对软件行为不了解的程序员,可以通过阅读单元测试 代码来理解软件的行为。同注释相比,单元测试还具有一个更好的特性:它是一种“可执行”文档。如果单元测试在被执行时无法通过,那么说明要么单元测试没有 反映当前代码的真实状况,要么说明代码中有bug。无论哪种情况,程序员都需要修改某一方,以保持两者的一致。
1.4.2 具有可测试性的软件具有更高的质量
近年来流行的极限编程方法论推崇“测试驱动开发”。我们认为,“测试驱动开发”并不一定要求必须先有测试后有代码,而关键在于要求在设计软件和 实现编码时,一定要预先把软件的可测试性考虑周全。这种可测试性的重要体现就是能够方便地将单个类或方法纳入单元测试之中。具有可测试性的软件的质量往往 高于不具有可测试性的软件,为什么这样说呢?
● 一个类能够被方便地纳入单元测试,往往说明这个类职责单一,也就是说它满足“单一职责原则”。
● 一个类能够被方便地纳入单元测试,往往说明它与其他类之间的耦合程度较低,相互依赖性较小,而且很可能满足“依赖抽象原则”和“开放-封闭原则”。
因此, 具有可测试性的软件,也更有可能是满足良好设计原则的软件,所以往往质量更高。