单元测试在一个完整的软件开发流程中是必不可少的、非常重要的一个环节。通常写单元测试并不难,但有的时候,有的代码和功能难以测试,导致写起测试来困难重重。因此,写出良好的可测试的(testable)代码是非常重要的。接下来,我们简要地讨论一下什么样的代码是难以测试的,我们应该如何避免写出难以测试的代码,以及要写出可测试性强的代码的一些最佳实践。
什么是单元测试(unit test)?
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常一个单元测试主要有三个行为:
- 初始化需要测试的模块或方法。
 - 调用方法。
 - 观察结果(断言)。
 
这三个行为分别被称为Arrange, Act and Assert。以java为例,一般测试代码如下:
@Test public void isPalindrome() { //初始化:初始化需要被测试的模块,这里就是一个对象。 //也可能没有初始化模块,例如测试一个静态方法。 PalindromeDetector detector = new PalindromeDetector(); //调用方法:记录返回值,以便后续验证。 //如果方法无返回值,那么我们需要验证它在执行过程中是否对系统的其他部分造成了影响,或产生了副作用。 boolean isPalindrome = detector.isPalindrome("kayak"); //断言:验证返回结果是否和预期一致。 Assert.assertTrue(isPalindrome); }
单元测试和集成测试的区别
单元测试的目的是为了验证颗粒度最小的、独立单元的行为,例如一个方法,一个对象。通过单元测试,我们可以确保这个系统中的每个独立单元都正常工作。单元测试的范围仅仅在这个独立单元中,不依赖其他单元。而集成测试的目的是验证整个系统在真实环境下的功能行为,即将不同模块组合在一起进行测试。集成测试通常需要将项目启动起来,并且可能会依赖外部资源,例如数据库,网络,文件等。
良好的单元测试的特点
1. 代码简洁清晰
我们会针对一个单元写多个测试用例,因此我们希望用尽量简洁的代码覆盖到所有的测试用例。
2. 可读性强
测试方法的名称应该直截了当地表明测试内容和意图,如果测试失败了,我们可以简单快速地定位问题。通过良好的单元测试,我们可以无需通过debug,打断点的方式来修复bug。
3. 可靠性强
单元测试只在所测的单元中真的有bug才会不通过,不能依赖任何单元外的东西,例如全局变量、环境、配置文件或方法的执行顺序等。当这些东西发生变化时,不会影响测试的结果。
4. 执行速度快
通常我们每一次打包都会运行单元测试,如果速度非常慢,影响效率,也会导致更多人在本地跳过测试。
5. 只测试独立单元
单元测试和集成测试的目的不同,单元测试应该排除外部因素的影响。
如何写出可测试的代码
我们从一个简单的例子开始探讨这个问题。我们正在编写一个智能家居控制器的程序,其中一个需求是在夜晚触摸到台灯时自动开灯。我们通过以下方法来判断当前时间:
public static String getTimeOfDay() { Calendar calendar = GregorianCalendar.getInstance(); calendar.setTime(new Date()); int hour = calendar.get(Calendar.HOUR_OF_DAY); if (hour >= 0 && hour < 6) { return "Night"; } if (hour >= 6 && hour < 12) { return "Morning"; } if (hour >= 12 && hour < 18) { return "Afternoon"; } return "Evening";
}
以上代码有什么问题呢?如果我们以单元测试的角度来看,就会发现这段代码根本无法编写测试, new Date() 代表当前时间,这是一个内嵌在方法里的隐含输入,这个输入是随时变化的,不同时间运行这个方法,返回的值也会不同。这个方法的不可预测性导致了无法测试。如果要测试,我们的测试代码可能要这样写:
@Test public void getTimeOfDayTest() { try { //

