求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
单元测试培训系列
 

发布于2011-06-24

 

单元测试培训系列:(一)单元测试概念以及必要性

说起单元测试,多数同学应该都知道或听过,可能不少同学认为自己也写过,甚至觉得单元测试很简单有什么好培训的?其实这个事情还真没想象的那么简单!我基本可以比较负责任的说,你若没深入对单元测试做过研究,不知道Mock对象为何物的话,那么可能你以前写过的单元测试压根就不是单元测试。

单元测试是什么?

这个问题其实并不太容易一两句话说得特别清楚。先借用下百度百科的定义:

单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

从以上这句定义我们可以看到,两个提取到到两个非常关键的字:最小粒度、隔离

  • 单元测试是测试的最小单位,必须可信任的,可重复执行的。
  • 如果测试的范围轻易的就会扩展到其他类或同类的其他方法,那就不再是最小单位,也就不是单元测试了!

例如:

类A中的方法CallMethod中调用了类B中的方法DoMethod,如果在编写测试的时候不把B类中的DoMethod隔离出来,造成单元测试CallMethod方法时,实际实行了DoMethod方法,那么这个测试方法不能算作是单元测试。(如何隔离会在后文详解)

单元测试的目的是什么?

有人曾给我一段非常简单的代码片段:一个方法,里面只是调用若干个其他方法,甚至也都没有任何返回值,然后问我这种代码写单元测试有任何价值?!完全是浪费体力!!

public class ClassA
{
public void CallMethod()
{
DoSomethingForYou();
DoSomethingForThem();
DoSomethingForMe();
}
}

其实提出这个疑问主要有两个原因导致:未能理解单元测试的目的是什么以及这段代码的可测试性并不高。

单元测试的目的是用来确保程式的逻辑如你预期的方式执行,而并不是用来验证是否符合客户的需求的!通过单元测试来建立一道坚实的保障,防止代码在日后的修改中不会被破坏掉。

是不是很失望?单元测试并不是用来验证代码是否符合需求的。

事实上,单元测试是白盒测试的一种,而且需要开发人员来完成,最好是谁开发的代码就该谁来编写单元测试代码,因为代码的编写者最熟悉该段代码的目的,进而编写出验证该目的是否达到的单元测试代码。

单元测试并不能用来代替其他测试手段,不过是实践过程中确实会很有效的帮助开发人员自查代码,进而发现一些潜在的BUG。但这只是一个额外的收获,若不是采用TDD那样的测试先行开发方式,那么单元测试的根本目的不是用于也无法检验当前代码是否存在BUG的!

上面有说到单元测试最好是由开发人员自己来编写,用于验证该段代码是否符合开发者开发的预期要求。这里可能会有个疑问,既然开发者自己已经很清楚自己想要 结果是什么,直接运行一遍代码实际跑一次,通过断点调试不是就可以很方便的验证了嘛?再通过编写代码的形式,甚至比开发这个功能本身更多的代码,去验证这 个方法是否符合编写的目的,不是很傻很笨很累的办法么?

也许通过一个大家经常会碰到的实际场景能更好的说明:

一个项目开始,项目经理把需求拆解为若干个模块分发给不同的开发人员去完成。这样每个人可能只熟悉自己的那部分代码。当项目某个阶段开发完成并上线后,可能部分开发人员会离开项目进入别的新项目,留下个别人员继续维护;或者项目下阶段开发新进一大批人员并不熟悉当前项目;当然最常见的是,在修改BUG阶段是无法完全做到谁产生的BUG就安排谁去修改。

那么这时候就会出现一种常见的情况:因为对当前代码要满足的各种目的不熟悉,在修改一个模块或者BUG的时候把原有正确的功能也影响到了!更要命的是,谁也不知道这个BUG出现了,等待测试人员需要去重新发现一遍。

于是项目经理会发现,每次只要做了代码修改,无论是重构还是功能新增修改,还是修改了BUG,都无法知道当前代码的健壮性,以前编写的东西是否依然正确可用?

然而如果这个项目在一开始就编写了单元测试的话,我们可以通过方便的自动化单元测试框架运行所有的单元测试,进而检查在此次修改前的所有被单元测试所覆盖的代码是否依然正常运行(符合以前编写的单元测试期望,如果验证通过,则认为原有代码未受到影响)

由上我们可以看出,单元测试虽然增加了相当大的开发工作量,但对于一个长期不断改进和维护的项目而言,单元测试反而是消减整体成本的一个有效手段,它能及时而准确的发现在代码修改之后,原来对代码要求的功能是否都依然正确满足。

但这里有个严重缺陷:单元测试无法检测到某个方法修改后是否对其他方法造成了影响,只能检测到被修改的方法本身的原有目的是否被影响(这个将在下面的与集成测试的区别中详解)

也因此,个人觉得单元测试最适合的场景是基于TDD开发。若需求发生改变,修改了一个方法,而多数情况下也会去修改单元测试代码,因为预期也发生了改变,这个时候又不能检测到对其他代码的影响,这时单元测试意义确实不大。

单元测试与集成测试的区别是什么?

多数人其实一直不能很好的区分集成测试和单元测试,甚至不少人一直理解的单元测试只能算是集成测试,但其实两者的概念是完全不同的。

单元测试测试的对象是每一个独立的方法,而且尽可能的隔离方法和其他方法以及其他外界依赖项;

基层测试的测试对象是被单元测试检测后的方法与方法之间的调用关系,以及调用执行过程是否符合预期。

  • 针对项目的一部份或全部进行测试,可以跨越不同的类别与方法,并可直接存取的外部资源。例如: File I/O, 数据库操作, 网络连接, …
  • 通常做集成测试都会需要先设置(Configure)测试所需的环境,测试完毕后通常要清除测试所产生的残留资料,以利下次测试或避免影响其他整合测试的结果。

    ClassInitialize Attribute

    ClassCleanup Attribute

    TestInitialize Attribute

    TestCleanup Attribute

以上这些属性常用于集成测试,不能出现在单元测试中。

单元测试的三大基本要素(Trustworthiness/Maintainability/Readability)

1、信任你的测试代码结果

1)你是否能信任你的测试结果?

2)当它通过,我们有信心说被测试代码一定工作。

3)当它失败,它一定证明被测试代码是错误的。

4)如果你不断的对测试结果失去信心,那么你也不会继续坚持撰写单元测试。有

5)许多人搞不清楚单元测试与集成测试的差别,以致于感觉自己写的单元测试过于薄弱而不相信测试的结果。

6)如果你因为某些原因导致测试失败,直接去改Code或直接去改Test Code都不是好事,你的首要目的是要能找出测试失败发生的主因,而非只是看错误这件事,这样你才能信任你的测试程式。

2、测试代码的可维护性

1)是否能够持续的维护你的测试程式?

2)如何有效的降低维护测试程式的成本?

PS:透过一些Testable Design Pattern 可以有效提升可维护性。例如: Repository Pattern, Service Pattern……

3、测试代码的可读性

1)你的测试程式的命名是否易于理解?

2)当你测试失败时是否能从测试失败的测试方法(TestMethod)明确看出实际失败的原因?

3)当读取测试数据的人看不懂你的测试,人们就不会执行这些测试、也不会去维护这些测试,久而久之就会越来越恶化。

Test Driven Development & Unit Test

写在本文最后,其实我一直觉得单元测试其实是为了TDD开发模式而诞生的,在这种开发模式下使用单元测试完全是非常顺畅的:

1、根据软件需求文档拆解软件功能,并设计出功能模块划分;

2、根据需要的功能模块设计出单元测试场景用例,因为此时可以很清晰的知道能够提供什么样的数据,以及需要达到什么样的功能,这对设计单元测试用例已经完 全足够了;

3、编写单元测试代码,这个时候可以专注于检验这个方法的是否满足设计的要求,此时甚至实际的代码还根本没开发,而.NET 4.0的Dynamic关键字在这里可以得到充分的发挥:调用那些根本都还不存在的方法,却不会导致编译无法通过。

4、若在编写单元测试过程中,可以预期当前这个方法若需要调用一些其他类或方法的支持,可以通过编写Mock Object来模拟,同样也是无需实现真正的代码,只需要有基本的代码框架或者接口即可。

5、在为这个方法编写好单元测试代码之后,就可以开始编写实际的代码实现了,因为在之前为了满足Testability的需要,代码已经是基于依赖倒置模 式的了,无需再担心其他需要调用的类或方法是否已经实现或正确实现。在编写好本方法的实现之后就可以通过运行之前的单元测试进行验收了。

可以看到,若按照以上这种方式进行开发,首先代码的耦合性是非常低的,其次代码的质量也是很高的,最后还会因为代码之间的耦合度低从而降低在开发过程中, 相互制约进度相互影响的可能性。在追查BUG的时候也很有优势:很容易查到BUG是否蔓延。

反之,对一个Legacy System进行重构使之Testable,再编写单元测试其实工作量不小,实际的收益也不会特别大。

单元测试的基本概念以及价值就基本讲完,下篇文章将开始介绍Visual Studio 2010中的单元测试工具与环境。

单元测试培训系列:(二)可测试性与重构

在单元测试培训系列:(一)单元测试概念以及必要性中,我们已经说过单元测试的定义是什么,里面有提到一个很重要的概念:隔离! 是的,没有隔离就没有可测试性,也就没有单元测试。

可测试性Testability

下面我们具体解释下什么叫做可测试性Testability:

让你的代码变的更加松耦合(Loosely coupled),让类与类之间的关联性降低,降低到可以个别独立存在,如此一来便可在彼此互不影响之下完成个别的单元测试,而这些类又能组合成一个有用的应用程式。

因为单元测试要尽可能的隔离与当前方法逻辑没有关系的方法以及外部资源(I/O文件,配置文件,数据库,网络以及静态变量等),即要求每段代码在不依赖其他额外方法以及外部资源的情况下依然可以正确执行!

这么说也许你无法理解也很难想象应该如何做到,那么我们下来举个例子说明。

publicclassTestability
{
publicdoubleComplexCompute(intx,inty,intz)
{
doubleresult;
CalcServiceWrapper wraper =newCalcServiceWrapper();
var a = wraper.Multiply(x, y);
result = wraper.Divide(a, z);
returnresult;
}
}

以上代码中,可以看到Testability类的ComplexCompute方法中依赖于CalcServiceWrapper类,并调用了该类的Multiply和Divide方法,最终完成了自身的功能。但这个代码就不具有很好的可测试性,因为ComplexCompute无法离开CalcServiceWrapper类而正确编译执行。那么如何重构这段代码使它可以解耦从而具有良好的可测试性呢?依赖倒置(Dependency Inversion)可以做到!

依赖倒置

依赖倒置是一种设计模式,具体的概念大家可以参见:向依赖关系宣战——依赖倒置、控制反转和依赖注入辨析, 在这里就不深入的去阐述依赖注入是怎么回事了。

其实,如果你已经仔细看完那篇文章或者你已经知晓那些知识,那么其实所谓的解耦重构也就很简单了。

依赖注入,有三种实现方式:属性注入,构造函数注入,方法参数注入;在实际项目中,属性注入是最常见的方式,我们下面就来用这种方式来对之前的代码进行解耦重构:

publicclassTestability
{
publicICalcService CalcService{get;set; }
publicdoubleComplexCompute(intx,inty,intz)
{
doubleresult;
var a = CalcService.Multiply(x, y);
result = CalcService.Divide(a, z);
returnresult;
}
}

如上代码中可以看到,把ComplexCompute方法中用到的CalcServiceWrapper类以及实例化的代码去掉,而增加一个类型为ICalcService(其实也可以是CalcServiceWrapper)的公开属性,在ComplexCompute方法中通过调用该属性来实现调用逻辑。这时候我们再来看,有没有发现,CalcService.Multiply方法和CalcService.Divide方法,无论CalcService是否被赋值,被赋予任何对象并不会影响当前方法的编译通过。换句话说:ComplexCompute方法不再依赖于CalcServiceWrapper类型是否实现以及如何实现。

可能说到这里,你依然无法理解,代码被重构后解耦了,但这跟可测试性又有多大的关系呢?很遗憾的是目前因为有个很重要的概念还没能介绍,在这里只能简单的提及,那就是Mock Object,将会在后面的章节详细介绍,这里先简单带过。所谓Mock Object其实就是个假对象,但它只是模拟一个对象中某个/些方法,输入某些参数,返回一个预先设定的值,不用真的去执行代码。这样在做单元测试的时候,我们可以给Testability类实例的CalcService属性设置一个Mock Object, 然后在我们测试其中的ComplexCompute方法的时候,我们可以保证里面的用到的CalcService方法都按照我们期望并设定的方式执行(返回的值),从而实现:我们测试Testability类中的ComplexCompute方法的时候,不会再担心CalcServiceWraper类的实现是否存在错误,或者发生什么问题,因为在ComlexCompute中执行的是Mock Object,而不是真正的CalcServiceWraper对象。我们的单元测试于是就只是测试ComplexCompute方法,不再涉及其他方法!

非可测试性场景Untestable Scene

上一段提到的是依赖倒置是最正规的符合Testability的代码模式,但实际应用中,我们可能无法做到完全很好的执行,但有几种场景我们是必须要特别注意并避免的。

1. 静态变量

静态变量是非常不符合Testability要求的(但在处理遗留系统Legacy System时会有特别的用处,后文会提及)。

首先来看静态变量,因为静态变量是全局性的,而且是保持有状态的。在单元测试培训系列:(一)单元测试概念以及必要性中,我们有说过,单元测试是测试的最小单位,必须可信任的,可重复执行的。因此,若使用静态变量,就会导致破坏单元测试的可重复执行性,并且可能干扰到别的单元测试方法。

例如:在方法A中改变了全局静态变量StaticVariableA的值,而在方法B中又要去尝试读取这个静态变量的值,那么在单元测试执行的过程中,若执行方法A和方法B的单元测试方法顺序不同,或执行多次很可能每次结果都会不同,这样明显会导致我们的单元测试结果不再可信。因此,我们应该尽量避免静态变量的使用。

2. 静态方法

静态方法的可测试性不强的原因倒不是因为静态方法自身有什么太大的问题,而是在编写单元测试的过程中受制于工具框架的限制:目前的开源Mocking Framework中都不支持对静态方法进行模拟,若大量使用静态方法,会在编写单元测试的时候遇到无法解耦的问题。当然利用一些商业Mocking Framework,如TypeMock,Moles等可以规避这个问题。

除了静态方法外,因为同样的开源Mocking Framework项目的限制,sealed类也是可测试性很差的代码。具体原因,我们放到后面的Mock Object章节一起介绍。

3. 直接依赖外部环境

一般来说,单元测试是要求代码尽可能不直接依赖于I/O文件接口,数据库,网络环境甚至系统环境(如时间等),但实际上一个项目总会有一部分代码无可避免的需要去访问这些外部资源,但我们可以尽量的控制这些代码集中在较小的范围内,并通过提取接口的方式使其他代码和这些直接访问外部资源的代码之间解耦。

因此,泛滥的在代码中随意使用访问外部资源的代码是非常Untestable的设计,例如:在程序中到处都是访问数据库的代码,DataContext出现在每层代码中;到处充满依赖于ASP.NET或者WCF环境的代码(HttpContext, HttpRequest等)。

我们没办法实现对边界代码的单元测试,只能通过集成测试来检验,例如对数据库访问层代码的测试;对网络访问层代码的测试等。但我们必须尽可能的防止这些边界代码的扩散,要尽可能的使他们被隔离起来。这个问题因为涉及面很广,暂不展开说明,在接下来的篇幅中会专门针对Entity Framework和ASP.NET MVC的Testability进行优化分析。

 
分享到
 
 
     


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...