理解单元测试(Unit Testing)

本文的目的是以最精炼的语言,正解什么是单元测试,为什么要单元测试,和如何进行单元测试。 

什么是单元测试(Unit Testing)?

测试(Testing)这个词很容易理解,那么什么是单元(Unit)呢?一个单元指的是应用程序中可测试的最小的一组源代码。一组源代码可测试,一般要求其有明确的输入和输出。因此,一般来讲,源代码中包含明确的输入和输出的每一个方法被认为是一个可测试的单元。注意,这里指的输出,并不局限于方法的返回值或对输入参数的改变,而包括了方法的执行过程中,改变的任何数据。

为什么要做单元测试?

单元测试的目的,是将应用程序的所有源代码,隔离成最小的可测试的单元,保证每个单元的正确性。理想情况下,如果每个单元都能保证正确,就能保证应用程序整体相当程度的正确性。

另一方面,单元测试也是一种特殊类型的文档,相对于书面的文档,测试脚本本身往往就是对被测试代码的实际的使用代码,对于帮助开发人员理解被测试单元的使用是相当有帮助的。 

如何进行单元测试?

隔离

要进行单元测试,首先就是要将被测试的单元,与所有外部依赖进行隔离。一般来讲,进行隔离主要有两种可选的技术,一种叫Stub,一种叫Mock。对于两个的区别和取舍,Martin Fowler有一个很经典的文章:Mocks Aren't Stubs。简单来说,如果要Stub或Mock一个方法,Stub指的是,为了测试,手动写一个相同签名的方法,根据我的测试需要,对给定的输入参数,返回一定的输出;而Mock则借助一定的框架组件,自动生成一个方法的实现,对于给定的输入,返回一定的输出。当然,所谓的Stub或Mock,并不单指对方法,尤其是在面向对象编程中,也可以是对属性,对对象或者对接口,等等。


为了方便实现隔离,对软件设计和开发的一个潜在的激励是,软件模块的设计人员和开发人员,不得不时时思考,在当前语言支持的各种特性的条件下,如何使得所写的代码,能够被方便的被隔离。虽然这看起来很像是一种限制,但是和软件设计的SOLID原则,其实是不谋而合的,因此,也就未尝不是一个优点了。

测试

解决了隔离的问题,我们就可以关注对一个单元的测试本身了。首先要提的一点是,所谓测试一个单元,一般往往是写一段测试脚本,给定输入,验证输出。但是,其实并非所有的单元测试都可以完全由计算机方便地自动完成的。尤其是如图形UI这样的比较抽象元素的判断。因此,从相对广义的角度来说,凡是能够用于验证一个被测试的单元正确性的所有方法进行的测试,都可以被认为是有效的测试。 

自动化

解决了隔离和测试本身的问题,下一个需要讨论的就是测试的效率的问题。尽管上面提到了,所谓单元测试,不完全都可以由计算机方便地自动完成,但是,对于那些可以由计算机自动完成的单元测试,如何提高写测试代码和运行测试的效率呢?答案是,陆续出现了很多的测试框架和测试工具。这些测试框架或工具,一般都会提供提高创建测试脚本效率的工具,并且,提供自动化执行测试和查看执行结果的功能,往往还可以很方便的和代码的自动化编译和部署进行集成,使测试自动化。

Guidelines

这里整理了一些如何写好测试代码的Guidelines:

  • 有测试总比没测试好
  • 测试尽可能的小而快
  • 使测试自动化
  • 进行代码覆盖检测
  • 在写任何新的实现代码或测试代码之前,先修复所有运行失败的测试
  • 再简单的代码也需要测试
  • 特别注意输入参数的边界案例
  • 不要只测试正常流程,还要测试可选和例外流程
  • 对于每一个报告的Bug,写一个对应的测试用例,方便回归测试
  • 时刻注意,所有测试都通过,不代表程序就真的100%没问题,测试永远只是辅助

英文资料

posted @ 2010-07-31 15:49 Teddy's Knowledge Base Views(1991) Comments(10) Edit 收藏

 回复 引用 查看   
#1楼2010-07-31 23:23 | gsgsdtc      
楼主有没有在实际的项目中推UT,在做单元测试的时候有没有遇到困难,我觉得“隔离”这一点不好做,如果要做到这一点,就会花N多的时间,大部分的项目都在赶工期,很多的时候UT都被忽略了。
 回复 引用 查看   
#2楼[楼主]2010-07-31 23:32 | Teddy's Knowledge Base      
@gsgsdtc
隔离这一点主要通过大量使用Mock来实现。很多时候,为了隔离不得不修改代码,使其方便测试。

至于全面推广,确实有一定难度,主要还是要在流程上施加一定的强制措施,如对每日集成,集成自动化的测试和测试代码的代码覆盖检查。否则,确实很容易被忽略。但是,即使只有部分单元测试,有也比没有强。:)

 回复 引用 查看   
#3楼2010-08-01 08:23 | 横刀天笑      
@gsgsdtc
在以前我也是这种想法,要做到很高的且有效的单元测试覆盖率很难,做到TDD更难。这个难有很多的理由,比如为了编写测试要花两倍的时间,而且为了隔离需要想出很多的办法,本身一个类要达到可测性的目标就对设计提出了很高的要求。
但是有了单元测试后,你基本上不需要开调试器了,你还记得有多少次你在Visual Studio里打个断点,然后单步执行?这个花费了很多的时间。

现在我发现单元测试抑或TDD,更多是一种做事方式的习惯而已。现在的公司所有人都是这种做事方式,倒觉得自然而然了,我们所有的功能代码都是由单元测试驱动出来的,没有单元测试不写代码。记得有一次我由于没有写单元测试就写了功能代码还被别人给rework了。

 回复 引用 查看   
#4楼2010-08-01 15:45 | gsgsdtc      
近来我也一直在思考如果做UT,如何能达到UT的效果又可以尽力减少写UT代码的时间,如果按楼主的这种方法,UT需要做业务代码的两倍的时间,我觉得很难在项目中推广。
http://www.cnblogs.com/gsgsdtc/articles/1789664.html
这是我对UT的一些思考,虽然TDD一直在提,可是真正做起来的,我还没有看到过。

 回复 引用 查看   
#5楼2010-08-01 15:52 | gsgsdtc      
@横刀天笑
我觉的UT最大的好处不是可以不开调试器,最大好处是,自动测试,持续的集成,和在重构后验证代码的正确性。在现实生活中,写UT不是一个习惯问题,在项目中赶工期的时候,如果要花两倍的时间,开发人员是不会做的,原来写业务代码就累得半死,现在我觉得很多人都有TDD的想方,可是没有一种行之有效的节约UT的开发时间,才让这个东东没有办法很好的推广的原因。

 回复 引用 查看   
#6楼[楼主]2010-08-01 16:24 | Teddy's Knowledge Base      
@gsgsdtc
要做到代码覆盖率100%的自动测试,持续的集成,的确不是一件容易是,非从管理的角度从上而下不可。相对来讲,只是TDD的话,性价比相对较高。请注意区别TDD和Unit Testing,虽然两者都可以自动化并且一起做回归测试,但是关注点是完全不同的。请参见我的另一篇文章,理解TDD:http://www.cnblogs.com/teddyma/archive/2010/07/30/1788364.html

 回复 引用 查看   
#7楼2010-08-02 21:37 | Silent Void      
我觉得单元测试的局限于下面的使用场景:
简单输入->(复杂)逻辑处理->简单输出
1. 简单输入: 容易构造测试数据,可以让覆盖虑尽可能地高;
2. 逻辑处理: 简单或复杂的数据/业务逻辑处理,越复杂越有必要测试;
3. 简单输出:容易校验测试结果;

但是很多场景下,都不符合上述的第一条和第三条;譬如很多应用系统,是数据以为中心的;数据存储在数据库,所谓的逻辑就是管理好这些数据。单独来看某个模块的,其输入可能是前面进行了N步处理得出的数据(具有较强的业务特征,分散存储在多个表),其输出也同样复杂;且测试只具有一次性,运行完毕后,由于业务逻辑限制,不太可能再将数据回滚到测试前的状态。
数据为中心,所以就无法被Mock;输入复杂,构造数据太麻烦,就更别谈覆盖率了;测试结果的很难校验;数据无法回滚,导致测试都是一次性的,同样的测试用例无法进行回归测试。

不知道楼主有没有遇到这种情况,是如何处理的呢?
以前看过那本经典的测试驱动开发(现在忘记差不多了),在网上也看过一些介绍单元测试的文章,都是在拿某段算法或数据无关的逻辑来举例,局限于上面我说的那种应用场景,而都对数据复杂性避而不谈。楼主能否谈下自己的理解及使用经验,谢谢。

 回复 引用 查看   
#8楼[楼主]2010-08-02 21:51 | Teddy's Knowledge Base      
@Silent Void
对于你举的例子“简单输入->(复杂)逻辑处理->简单输出”的单元测试,是最没有技巧的。因为,无需在设计时和写测试时考虑隔离的问题。事实上,只要应用合理的设计技巧使得被测试的代码可以进行隔离,合理利用测试工具,所有类型的代码,理论上,只要可以由计算机判断对错,都是可以进行单元测试的。

以你举的数据为中心的代码的测试来说,一般的做法是,先设计好想要测试什么,在每次测试执行前,初始化数据库中被测试的数据(清空,插入测试数据,等等),运行测试,运行完再对数据库中的测试数据做一些清理,这样就同样能做自动化的测试了。这样做虽然成本并不低,但是,对于重要功能来说,稍高的测试成本总好过运行时才发现问题。

 回复 引用 查看   
#9楼2010-08-12 09:15 | Silent Void      
@Teddy's Knowledge Base
单元测试也基本上就限于简单输入->(复杂)逻辑处理->简单输出
这种场景,复杂点儿的,也就在理论上玩玩,用起来都是鸡肋。
单元测试,说白了就是把白盒测试细化、提前到开发阶段,开发人员构造输入,校验输出。这东西也有很大的局限性,只是宣传的时候都扬长避短,被吹玄了。

 回复 引用 查看   
#10楼2010-09-11 21:17 | 菜阿彬      
“简单来说,如果要Stub或Mock一个方法,Stub指的是,为了测试,手动写一个相同签名的方法,根据我的测试需要,对给定的输入参数,返回一定的输出;而Mock则借助一定的框架组件,自动生成一个方法的实现,对于给定的输入,返回一定的输出。”
——————————————————
这段的意思是:Stub和Mock的区别在于前者是手工写的,后者是借助框架?如果是这样,我不同意。
我的理解:UT里分“测behavior”和“测state”两种,Mock是可以对行为进行验证的,Stub则只能对状态进行测试。