2008年2月19日

学习单元测试

说到单元测试,我想很多朋友都听说过或者学过一点,但是真正贯彻执行的少之又少。我也是这样的 :)

为什么需要再学习一下单元测试呢?因为我现在正在进行的项目中遇到了很多的Bug,当然称之为Bug也不完全对,因为在最初编程的时候没有考虑到会出现这些情况。我想通过一种方法来加速我完成项目的时间以及提高完成代码的质量。

忽然在Yiyan上看到这样一篇文章《实践的一小步-代码质量的一大步》,其中提到了提高代码质量的三招实践攻略:单元测试,代码覆盖,持续整合。读罢顿觉不错,好的项目都是需要这个东西的。其实再仔细想想我们经常提到的发表论文前的相关实验,我敢保证,很少有人说他的实验部分的代码是100%没有Bug的。很多时候,实验结果都是会受到代码质量的影响的。

为了弥补自己软件开发方面的不足,我打算从单元测试开始学习。争取以后都能在写代码前写好测试用例。

在Google上随便转了一下,发现这篇文章《单元测试》写得很好。在提到测试用例和实际开发的顺序时,作者的态度是“在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适”。他的结论是“先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。”感觉这个经验相当不错,之所以很多人不愿意写测试用例,我想也是和没有找到这样的经验有关。

对于单元测试,一些流行的误解如下:

  • 它浪费了太多的时间
  • 它仅仅是证明这些代码做了什么
  • 我是个很棒的程序员, 我是不是可以不进行单元测试?
  • 不管怎样, 集成测试将会抓住所有的Bug
  • 它的成本效率不高

这里就不一一解答这些误解了,感兴趣的朋友可以看看《单元测试》中的解释。

单元测试的优点有:

  • 它是一种验证行为

程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。

  • 它是一种设计行为

编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。

  • 它是一种编写文档的行为

单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

  • 它具有回归性

自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。

单元测试主要包括黑盒测试以及白盒测试,黑盒测试主要是在函数功能上进行测试,不会深入到函数内部;白盒测试是对函数的逻辑进行测试,需要深入到函数的内部,需要覆盖的逻辑单位主要有:语句、分支、条件、条件值、条件值组合,路径。语句覆盖就是覆盖所有的语句,其他类推。另外还有一种判定条件覆盖,其实是分支覆盖与条件覆盖的组合。跟条件有关的覆盖就有三种,解释一下:条件覆盖是指覆盖所有的条件表达式,即所有的条件表达式都至少计算一次,不考虑计算结果;条件值覆盖是指覆盖条件的所有可能取值,即每个条件的取真值和取假值都要至少计算一次;条件值组合覆盖是指覆盖所有条件取值的所有可能组合。作者做过一些粗浅的研究,发现与条件直接有关的错误主要是逻辑操作符错误,例如:||写成&&,漏了写!什么的,采用分支覆盖与条件覆盖的组合,基本上可以发现这些错误,另一方面,条件值覆盖与条件值组合覆盖往往需要大量的测试用例,因此,在作者看来,条件值覆盖和条件值组合覆盖的效费比偏低。

作者认为效费比较高且完整性也足够的测试要求是这样的:完成功能测试,完成语句覆盖、条件覆盖、分支覆盖、路径覆盖。这种测试要求其实是比较高的,但是借助于一些相关的工具还是能方便的完成的。

 

工具篇:

        单元测试最著名的工具我想就是Unit系列吧,C++的CppUnit,Java的JUnit,Python的PyUnit等等,还有一些其他工具吧。由于目前我主要采用Java进行编程,搜罗一下Java的单元测试工具(Eclipse下使用的):JUnit4, TestNG。阅读了一篇关于这两个重量级工具的对比文章(《追求代码质量: JUnit 4 与 TestNG 的对比》)后,我决定使用TestNG来作为我的单元测试工具。

 

经过三个小时的尝试后,不得不放弃TestNG,原因是它不支持Eclipse 3.3,感觉好像支持jdk1.6也不太好,似乎TestNG跟不上时代啊!再一眼,发现Junit到是默认出现在了Eclipse 3.3里面,那就试试Junit4咯。呵呵,只要能提高效率就要,为什么一定要用TestNG呢 :)

 

学习几个相关的JUnit教程,感觉这个系列最好:

  1. 在Eclipse中使用JUnit4进行单元测试(初级篇)
  2. 在Eclipse中使用JUnit4进行单元测试(中级篇)
  3. 在Eclipse中使用JUnit4进行单元测试(高级篇)

学完后有这几个心得:

1. Eclipse下创建单元测试非常方便,很多时候只需要点击鼠标即可完成绝大多数的工作,而且可以通过创建很多的单元测试后用打包的方式全部执行。

2. JUnit4比JUnit3多出了许多TestNG具有的特性

3. JUnit4主要有如下几点:

  1. 包含必要地Package
  2. 测试类的声明
  3. 测试方法的声明,以@开头,例如@Before、@Test、@After
  4. 忽略测试某些尚未完成的方法@Ignore
  5. 限时测试,防止死循环,用timeout如@Test(timeout = 1000)
  6. 测试异常,用expected 如 @Test(expected = ArithmeticException.class)
  7. 选择运行器@RunWith(...)
  8. 参数化测试,用于实例数较多的测试,用@RunWith(Parameterized.class)配合Collection的asList
  9. 打包测试,import org.junit.runners.Suite; @RunWith(Suite.class)

4.JUnit4的功能非常强大,自己以后的编程习惯应该是先写整个程序的类及函数框架,然后生成大量的对各个相关函数的单元测试,并且写好打包测试,然后每次实现一个函数就运行一次打包测试,从而逐渐消除所有的Failure和Error,其间如果需要对类及函数框架做出增删改,都必须立即更新单元测试的结构和代码,直到完成

5. 对于具有单元测试的项目,代码的组织可以是在当前文件夹下新建两个文件夹java及test,分别存放正式的代码文件和测试文件,这样有利于最终的文件提取。

 

哈哈,一口气写了这么多,就这样吧!算是自己对单元测试的一次较为深入的学习。

没有评论: