单体测试指南
 
1. 单体测试应该小并且快
    理想情况下在每次代码签入之前都要执行下测试套件。测试快就可以降低开发周转时间。
2. 单体测试必须完全自动化,不需要交互
    测试套件通常经常执行,必须完全自动化才有用。如果结果需要人工检查,该测试就不是真正的单体测试。
3. 单体测试应便于运行
    开发环境应该配置成通过一个单独的命令或者一个按钮点击就可以运行单独的测试和测试套件。
4. 评估测试
    对测试的运行进行覆盖分析从而得到执行覆盖并调查代码的哪些部分执行,哪些部分没有执行。
5. 立刻修复失败测试
    每个开发者都应该确保在签入后新的测试以及所有既有的测试都可以成功运行。
如果在日常测试执行中有一个测试失败了,整个团队必须停下手头的工作来保证问题得到解决。
6. 保持测试在单体层
    单体测试和类的测试相关。每个普通类都要有一个测试类,类的行为应该在隔离的条件下测试。应该避免使用单体测试框架测试整个工作流程,因为这样的测试会很慢并很难维护。会有需要流程测试的地方,但不应该作为单体测试的一部分,它必须独立地设置和执行。
7. 从简单开始
    一个简单的测试要比根本没有测试好。一个简单的测试类可以建立起目标类的测试框架并可以验证编译环境、单体测试环境、执行环境和覆盖分析工具是否具备以及是否正确,从而可以证明目标类是否是程序集的一部分以及是否可以访问。
单体测试的入门程序可能像这样:
InBlock.gif                 void testDefaultConstruction()
InBlock.gif                {
InBlock.gif                        Foo foo = new Foo();
InBlock.gif                        assertNotNull(foo);
InBlock.gif                }
8. 保存测试相互独立
    为了确保测试健壮并简化维护,测试不能依赖其它测试以及测试执行的先后顺序。
9. 测试类和被测试类尽量近
    如果被测试类是Foo,那么测试类就应该命名为FooTest(而不是TestFoo)并同Foo放在同一个包里面。将测试类放在单独的目录下会使其难于访问和维护。
确保编译环境的配置可以使得测试类不会进入生产库或执行文件中。
10. 合理命名测试
    确保每个测试方法测试一个明显的类功能并据此命名测试方法。典型的命名规则是test[what],比如testSaveAs(), testAddListener(), testDeleteProperty()等。
11. 测试公开API
    单体测试被定义为通过公开API测试类。有些测试工具可以实现类的私有方法的测试,但由于这会使得测试太过繁琐并更难于维护因此需要避免。如果有一些类的私有方法需要显示地进行测试,考虑将其重构成工具类的公开方法。要这样做就应该要改进总体设计,而不是仅仅为了帮助测试。
12. 以黑盒考虑
    作为第三方的类使用者来测试该类是否满足需求。尝试着让它失效。
13. 以白盒测试考虑
    毕竟,开发者写测试的同时也写了被测试类,需要特别注意测试复杂逻辑。
14. 微不足道的类也要测试
    有人会推荐测试所有主要的情况,而可以忽略诸如简单的类似setter和getter等微不足道方法。 然而,应该测试微不足道方法情况有几个原因:
  • 很难定义微不足道。对不同的人可能有不同的含义。
  • 从黑盒测试的角度看无法知道代码的哪部分是微不足道的。
  • 由于拷贝-粘贴操作,微不足道的代码也可能包含错误。
InBlock.gif                 private double weight_;
InBlock.gif                 private double x_, y_;
InBlock.gif                 public void setWeight( int weight)
InBlock.gif                {
InBlock.gif                        weight = weight_;     // 错误
InBlock.gif                    }
InBlock.gif                 public double getX()
InBlock.gif                {
InBlock.gif                         return x_;
InBlock.gif                }
InBlock.gif                 public double getY()
InBlock.gif                {
InBlock.gif                         return x_;     // 错误
InBlock.gif                    }
 
    建议就是测试所有代码,毕竟微不足道的代码很容易测试。
15. 首先关注执行覆盖率
    执行覆盖不同于实际代码覆盖。一个测试的最初目标应该保证高的执行覆盖。这可以确保代码在某些参数下真正执行。有了这个,就可以去改善测试覆盖了。注意实际代码覆盖很难确定(通常都很接近0%)。
考虑下面这个公开方法:
InBlock.gif             void setLength( double length);
    通过调用setLength(1.0) 你就可能得到100% 的执行覆盖率。为了达到真正100%的实际测试覆盖,必须使用所有可能的double值来调用该方法以确认它们的正确行为。这显然是不可能的。
16. 覆盖边界情况
    确保覆盖参数边界的情况。对于数,测试负数、0、正数、最小、最大、NaN、无穷等情况。对于字符串,考虑空字符串、单个字母的字符串、非ASCII的字符串、多字节的字符串等情况。对于集合,测试空集合、单个元素集合、第一个、最后一个等。对于日期,考虑1月1日、2月29日、12月31日等。被测类会提示各个具体情况下的边界情况。由于这些都可能是错误的根源,因此要尽可能多地测试这些情况。
17.提供随机数生成器
    在覆盖了边界情况后,进一步提高覆盖率的一个简单方法就是产生随机数以使得每次测试都可以使用不同的输入执行。
为了实现这个目标,可以提供一个生成double、integer、 string和dates等类型随机数的实用类。生成器必须可以从各个类型的全范围内生成值。
如果测试很快,可以考虑在一个循环内运行尽可能多的组合。下面的例子就是验证通过一次大端和一次小端转化是否可以得到原值。由于测试很快,每次可以根据不同的值执行100万次。
InBlock.gif                 void testByteSwapper()
InBlock.gif                {
InBlock.gif                         for ( int i = 0; i < 1000000; i++)
InBlock.gif                        {
InBlock.gif                                 double v0 = Random.getDouble();
InBlock.gif                                 double v1 = ByteSwapper.swap(v0);
InBlock.gif                                 double v2 = ByteSwapper.swap(v1);
InBlock.gif                                assertEquals(v0, v2);
InBlock.gif                        }
InBlock.gif                }
18. 每次测一个功能
    在测试模式下,很容易去尝试对每个测试中的每件事都断言。这必须避免因为它使得维护更难。仅测试该测试方法的名字表明的功能。
对于一般代码应该将测试代码尽可能少作为一个目标。
19. 使用显式断言
    在assertEquals(a, b)和assertTrue(a == b) (之类的)之间优先选择前者,因为它可以在测试失败的时候给出关于失败原因的更多有用的信息。 这对于上述提到的随机参数组合时输入值无法预先知道的情况尤为重要。
20. 提供负面测试
    负面测试是通过故意用错代码以验证稳健性和适当的错误处理。
    考虑下面这个如果传入负数就会抛出异常的方法:
InBlock.gif void setLength( double length) throws IllegalArgumentException;
    可以这样来测试该特殊情况的正确行为:
InBlock.gif                 try {
InBlock.gif                    setLength(-1.0);
InBlock.gif                    fail();     // If we get here, something went wrong
InBlock.gif                }
InBlock.gif                 catch (IllegalArgumentException exception) {
InBlock.gif                     // If we get here, all is fine
InBlock.gif                }
21. 设计代码是考虑测试
    编写和维护单体测试的代价很高,使公开API最小化并降低代码的循环复杂度是降低成本并使得高覆盖率的测试代码更快编写和更易于维护的方式。
一些建议:
  • 通过构造时候确定状态来使得成员类不可变。这会减少对setter方法的需求。
  • 限制过渡使用继承和虚的公开方法。
  • 减少利用了友元类(C++)或包范围(Java)的公开API。
  • 避免不必要的分支。
  • 在分支内的代码尽可能少。
  • 尽可能使用异常和断言来验证分别在公开和私有API中的参数。
  • 限制使用帮助方法。从一个黑盒测试的角度每个方法都必须一样地测试。考虑如下这个小例子:
InBlock.gif                 public void scale( double x0, double y0, double scaleFactor)
InBlock.gif                {
InBlock.gif                         // scaling logic
InBlock.gif                }
InBlock.gif
InBlock.gif                 public void scale( double x0, double y0)
InBlock.gif                {
InBlock.gif                        scale(x0, y0, 1.0);
InBlock.gif                }
     省去后面的就可以简化测试,但代价是客户端代码会有额外的工作。
22. 不要关联到预定义的外部资源
     编写测试文件的时候应该不能利用将要执行环境的上下文信息以使得他们可以在任何时间任何地方运行。为了给测试提供所需的资源,这些资源应该通过测试自身提供。
现在考虑解析某种类型文件的类的情况。应该将文件的内容放在测试内,在测试开始前将其写入一个临时文件并在测试完成后删除掉文件,而不是从预定义的路径选择一个示例文件。
23. 了解测试成本
    不写单体测试代价很高,写单体测试代价也很高。这是两者之间的平衡,从执行覆盖率考虑一般的行业标准是大约是80%。
通常比较困难达到完全覆盖的区域是处理外部资源的错误或异常。在一个交易中模拟数据库崩溃是完全有可能的,但较之作为替代方法的深度代码检查通常代价过高。
24. 测试优先级排序
    单体测试通常是一个至底而上的流程,如果没有测试系统所有部分所需的足够资源就应该优先考虑最底层。
25. 考虑到测试代码失败
    考虑下这个简单的实例:
InBlock.gif        Handle handle = manager.getHandle();
InBlock.gif        assertNotNull(handle);
InBlock.gif
InBlock.gif        String handleName = handle.getName();
InBlock.gif        assertEquals(handleName, "handle-01");
    如果第一个断言是假的,接下来的语句就会崩溃,余下的测试也就不会被执行了。所以应该考虑单个测试代码失效不会使得这个测试套件无法执行。通常可以改写成这样:
InBlock.gif        Handle handle = manager.getHandle();    
InBlock.gif        assertNotNull(handle);    
InBlock.gif         if (handle == null) return;    
InBlock.gif
InBlock.gif        String handleName = handle.getName();    
InBlock.gif        assertEquals(handleName, "handle-01");
26. 编写测试来重现缺陷
    如果报告了一个缺陷,就应该写一个测试来重现该缺陷(比如一个失败的测试)并使用该测试作为是否成功修复代码的标准。
27. 认识局限性
    单体测试永远不能证明代码的正确性!
    一个失败的测试意味着代码含有错误,而一个成功的测试不能证明任何东西。
单体测试的最普遍的应用就是验证和记录底层的需求以及回归测试:验证代码在变迁和重构的过程中一直保持稳定。
    因此,单体测试永远无法替代前期设计和健全的开发流程。单体测试可以作为既有开发方法的一个宝贵补充。
 
后记:
    对于第9点”测试类和被测试类尽量近”和第14点”微不足道的类也要测试”,我不是很赞同,实践的时候也没有完全遵守。但为了忠实于原文没有做擅自修改。大家在学习和实践的时候可能也会接触一些相悖的观点和理论,不要担心,选择最适合你尝试下就知道了。同时,欢迎说出你的故事!
 
Geotechnical Software Services
Copyright @ 2007