Tin's Blog

You are coming a long way, baby~Thinking, feeling, memory...

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  128 随笔 :: 0 文章 :: 221 评论 :: 0 Trackbacks

本文翻译自IBM DeveloperWorks上的一篇文章,该文讲述了测试分类(test categorization)的概念,本身这个概念很简单,但是却实际的解决我们常见的问题,在我们的测试庞大到一定地步的时候,测试的运行时间过长,维护成本很高,我们如何能够保证持续集成(CI)的正常运行?那就是通过测试分类。所以我翻译了这片文章,希望对大家有所帮助。

原文:In pursuit of code quality: Use test categorization for agile builds
原文作者:Andrew Glover is president of Stelligent Incorporated, which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. Check out Andy's blog for a list of his publications.

大家都同意开发人员的测试很重要,但是为什么要花这么长的时间运行测试呢?这个月,Andrew Glover将给我们讲述对于系统来说需要保证运行的三类测试,并且告诉你如何根据分类整理和运行测试。结果将会奇迹般的减少build的时间,即使是面对当今庞大的测试集。
如果不太难过的话,假想一下你是一个2002年初刚刚建立的公司的开发人员。在淘金热潮中,你和你的同事已经决定使用最流行最强大的Java API来开发一个庞大的数据驱动的Web应用程序。你可你的管理团队坚定的信仰敏捷过程。从第一天开始,就使用JUnit编写测试,并且通过Ant build脚本尽可能频繁的运行它们。最后,你们还会使用cron(*nix下的一个定时运行脚本的任务)来进行nightly build。再然后,某些人可能会下在CruiseControl然后把测试写成套件,然后在每次check-in时执行(持续集成)。
现在回到今天。
经过了前几年的磨练,你的公司已经开发了数量巨大的代码,当然也有同样庞大的JUnit测试。一年前所有的事情都运转良好,当你的测试套件有超过2000个测试,人们开始注意到build过程可能要执行三个小时以上。几个月以前,你停止通过代码提交来处罚持续集成(CI)运行单元测试,因为CI服务器会因此过渡繁忙。你改为进行nightly测试(每日测试),第二天早上开发人员可能会头疼测试为何失败。
最近,测试套件似乎很难在晚上运行一次以上了——这是为什么呢?它们永远运行不完!没有人会用几个小时的时间来等待确认代码运行是正常的(或不正常)。所以,整个的测试会在晚上运行,对么?
因为你如此频繁的运行测试,他们总是充满了问题。(译者注:你会开始怀疑是不是测试也出了问题,是否想测试你的测试?)从而,你和你的团队开始怀疑单元测试的价值:如果代码质量并不那么重要,为什么我们要承受这种痛苦?假如你可以用敏捷的方法运行它们的话,你们完全同意这是单元测试的基本价值。

尝试测试分类(test categorization)

你需要的是一个让你的build转变到更敏捷状态的策略。你需要一种解决方案来允许你在一天内多次运行测试,让那些已经需要三个小时完成的测试回到原先的状态。
在你尝试使用这个策略让你的测试套件恢复原形之前,思考一下“单元测试”的基本概念可能会有所帮助。“我家有一只动物”和“我喜欢汽车”这样的陈述不是非常明确,所以,不幸的是,“我们编写单元测试”也不明确。现在,但愿测试泛指一切。
思考前面的两个关于动物和汽车的陈述:它们产生了很多疑问。例如,你家里有什么动物?是猫、蜥蜴还是熊?“我家有一只熊”与“我家有一只猫”完全不同。同样的,“我喜欢汽车”对于与汽车销售商交谈时没有帮助。你喜欢哪种车:运动车、卡车或者大货车?不同的答案会将你引入不同的路径。
同样,对于开发人员进行测试,根绝测试类型分类是有所帮助的。这样做更加精确,能够允许你的团队以不同的频度运行不同类型的测试。分类是避免恼人的运行所有“单元测试”的三小时build的关键方法。



三种分类

形象的将你的测试套件整理为三层,每一层代表开发人员进行的不同类型的测试,它们是根据运行时间的长短划分的。如图1所示,每一层将花费更多的总build时间,无论是运行时间还是编写它们所需的时间。

图1 测试分类的三层


最下面一层测试运行时间最短,如你所想,他们也是最容易写的。他们也覆盖最少量的代码。顶层是有高层次的测试组成,它们检测应用程序的很大一部分。这些测试相对难写,同时也需要更多时间来执行。中间一层测试介于两个极端之间。
这三个分类如下:

  • 单元测试
  • 组件测试
  • 系统测试
让我们分别的考察它们。

1、单元测试

单元测试隔离的确认一个或者多个对象。单元测试不处理数据库、文件系统或者任何可能带来测试不能保证长期可运行的因素;顺序上,测试可以从(项目)第一天就开始写。事实上,这就是JUnit的设计目标。单元测试的隔离概念是在很多mock对象库隔离特定对象的外在依赖的基础上的。进一步说,单元测试可以在实际代码编写前就开始写——也就是测试先行开发TDD的概念。
单元测试一般容易编写,因为他们不依靠于系统依赖,并且他们运行迅速。不好的方面是,单独的单元测试只能提供有限的代码覆盖度。单元测试的价值在于允许开发者在最低的依赖程度下保证对象的质量。
因为单元测试运行迅速容易编写,一个代码库应该有很多单元测试且尽量频繁的运行它们。你应该在每次build的时候运行它们,不管是在你的机器或者一个CI环境(以为这你应该在每次向SCM系统chech in之前运行它们)。

2、组件测试

组件测试保证多个对象的交互,但是它们突破了代码隔离的概念。因为组件测试处理多层架构,他们经常要处理数据库、文件系统、网络元素等。而且组件测试一般很难在(项目)前编写,所以将它们加入到一个实际的测试先行/测试驱动的场景中是个很大的挑战。
组件测试编写要花多一些时间,因为他们比单元测试要棘手。从另一个方面来看,他们能够提供比单元测试更高的代码覆盖率因为它们的宽工作范围。它们运行耗时更多,所以它们会极大地拖长你们的总测试耗时。
一个宿主框架可能减少测试庞大架构组建的挑战难度。DbUnit就是一个这种框架的完美例子。DbUnit是编写依赖于数据库的测试容易,它能够处理复杂的数据库状态准备工作。
当测试引起build时间延长,你基本上可以确定那就是大组的组件测试造成的。因为这些测试比单元测试运行时间更长,你可能发现你不能总是运行它们。因此,它让CI环境至少以小时为间隔执行它们。你一应该要求每个开发者在check in前在本机环境运行这些代码。

3、系统测试

系统测试从端到端保证软件应用。因此,他们提出了高度的架构复杂性:整个应用必须在进行系统测试时运行。如果是一个Web应用程序,你需要访问数据库,从Web服务器、(应用程序)容器、任何相关的配置都要配合系统测试的运行。系统测试总是在软件开发周期的最后阶段撰写的。
系统测试对于编写人员是个挑战,并且实际往往花费比较长的时间。另一方面,他们提供更好的催款理由,也就是说,他们提供了系统架构级的代码覆盖率。
系统测试与功能测试非常相近。区别在于它们不是一个假扮用户,用户是虚拟的。就像组件测试一样,很多框架都是来帮助这类测试的。例如,jWebUnit通过模拟一个浏览器提供了测试Web应用程序的基础设施。

什么是接受测试?
接受测试与功能测试类似,不同点在于,理想情况下,客户或者最终用户来编写接受测试。与功能测试类似,接受测试按照最终用户的行为测试。一个备受关注的接受测试框架是Selenium,它使用浏览器来测试Web应用程序。Selenium可以在build过程中自动运行,就像JUnit测试一样。但是Selenium是一个新的平台:他不一定使用JUnit,方式也不太一样。(Selenium RC就没有这个问题了)

我应该使用jWebUnit或者Selenium?
jWebUnit是一个JUnit扩展框架,设计用来进行系统测试;所以,它需要你自己写这些测试。Selenium是一个优秀的接受测试和功能测试工具,不同于jWebUnit,它允许非程序员编写测试。理想状态下,你的团队可以同时使用两种工具来确认应用程序的功能。

使用TestNG进行测试分类
使用TestNG实现测试分类非常容易。使用TestNG的group注释,逻辑上将测试分类就是进行合适的group注释,这非常简单。运行某一分类的测试只需要将group名称传给test runner就可以了,例如通过Ant。

实现测试分类

所以,你的单元测试套件实际上是单元测试、组件测试和系统测试的套件。甚至,在你检查所有的测试后发现build需要这么长时间是因为大部分测试都是组件测试。下一个问题是,如何通过JUnit实现测试分类?
你有很多选择,但是让我们先试验一下最简单的两个:

  • 根据需要的分类创建不同的JUnit套件(suite)文件
  • 对于不同类型的测试创建不同的目录

创建不同的套件

你可以使用JUnit的TestSuite类(它也是一种Test)定义一组同类测试的集合。你要创建一个TestSuite的实例并添加相关的测试类到test方法中。你可以在TestSuite实例中通过定义一个叫做suite()的public static方法告诉JUnit这个套件包括哪些测试。所有包括的测试将会一次全部执行。因此你可以通过创建TestSuite来实现测试分类,一个单元测试的TestSuite、一个组件测试的TestSuite,有一个系统测试的TestSuite。
例如清单1的类中的suite()方法创建了一个包含所有组建测试的TestSuite。注意这个类不是非常符合JUnit规范。他既没有继承TestCase,也没有任何测试的定义。但是JUnit会自动发现suite()方法并且运行它返回的所有测试类。

清单1 单元测试的TestSuite

package test.org.acme.widget;

import junit.framework.Test;
import junit.framework.TestSuite;
import test.org.acme.widget.*;

public class ComponentTestSuite {

 
public static void main(String[] args) {
  junit.textui.TestRunner.run(ComponentTestSuite.suite());
 }

 
public static Test suite(){
  TestSuite suite 
= new TestSuite();
  suite.addTestSuite(DefaultSpringWidgetDAOImplTest.
class);
  suite.addTestSuite(WidgetDAOImplLoadTest.
class);
  
  suite.addTestSuite(WidgetReportTest.
class);
  
return suite;
 }
}


定义TestSuite的过程需要你察看你当前的所有测试并将它们加入到相应的类里面(例如,所有的单元测试加入到UnitTestSuite)。这也就意味着你在相应的分类里面创建了新的测试,你必须编程式的将它们添加到合适的TestSuite中,当然还需要重新编译它们。
运行单独的TestSuite需要单独的Ant任务来运行正确的测试组。你可以定义一个component-test任务来执行ComponentTtestSuite,就像清单2中的样子:

清单2 运行组建测试的一个Ant任务

< target  name ="component-test"  
           if
="Junit.present"  
           depends
="junit-present,compile-tests" >
 
< mkdir  dir ="${testreportdir}" />    
 
< junit  dir ="./"  failureproperty ="test.failure"  
             printSummary
="yes"  
             fork
="true"  haltonerror ="true" >
   
< sysproperty  key ="basedir"  value ="." />      
   
< formatter  type ="xml" />       
   
< formatter  usefile ="false"  type ="plain" />      
   
< classpath >
    
< path  refid ="build.classpath" />        
    
< pathelement  path ="${testclassesdir}" />         
    
< pathelement  path ="${classesdir}" />       
   
</ classpath >
   
< batchtest  todir ="${testreportdir}" >
    
< fileset  dir ="test" >
     
< include  name ="**/ComponentTestSuite.java" />                  
    
</ fileset >
   
</ batchtest >
 
</ junit >
</ target >

理想情况下,你还需要一个触发单元测试的任务和系统测试的任务。最后,还有希望运行所有测试的情况,你需要创建第四个任务来运行其它三个任务,就像清单3里面那样:

清单3 运行所有测试的任务

< target  name ="test-all"  depends ="unit-test,component-test,system-test" />

创建单独的TestSuite是一个迅速实现测试分类的解决方案。缺点是这个方法需要你创建新的测试,你必须编成式的将它们添加到合适的TestSuite里面,这可能有点痛苦。给每个测试类型创建单独的目录可能是一种更加有弹性的方法,它允许你添加新的测试分类但无需重新编译。


创建单独的目录

我发现最简单的通过JUnit实现测试分类的方法是逻辑上将不同类型的测试放到不同的目录中。使用这个方法,所有的单元测试都放在unit目录,所有的组建测试都放在component目录,等等。
例如,在test目录中保存着所有未分类的测试,你可以创建三个新的子目录,就像清单4中那样:

清单4 实现测试分类的目录结构

acme-proj/
       test/
          unit/
          component/
          system/ 
          conf/

运行这些测试,你需要定义至少四个Ant任务:一个给单元测试,另外的给组建测试,还有系统测试。第四个任务是一个方便运行其它三个测试类型的任务(就像清单3种展示的那种方式)。
JUnit任务就像清单2中的形式。区别在哪里呢,只是在任务的batchtest这个地方。这次,fileset指向的是一个指定的目录,就像清单5种的样子,他指向了unit目录:

清单5 JUnit任务中的batchtest方面,用来运行所有单元测试

< batchtest  todir ="${testreportdir}" >
 
< fileset  dir ="test/unit" >  
  
< include  name ="**/**Test.java" />        
 
</ fileset >
</ batchtest >

注意这个任务运行test/unit目录下的所有测试,当创建了新的单元测试(或者其它分类的其它测试),你只需要把它们放到这个目录里面就可以了!这比添加一行到TestSuite中并重新编译它要方便多了。


问题解决了!

回到最初的场景,我认为你和你的团队会决定使用单独的目录这种弹性的解决方案来解决你们的build时间过长的问题。这个任务最难的一个方面是检查和分清测试类型。你重构你的Ant build文件创建四个新的任务(三个单独的测试分类还有一个运行它们三个)。甚至,你修改CuiresControl只在check-in的时候运行单元测试,而组建测试按小时运行。更进一步的检查后,系统测试也可以几个小时运行一次,也许你会创建一个新的任务来同时运行组建测试和系统测试。

最后的结果是每天测试运行很多次,你的团队可以快速的发现集成错误——一般在几个小时内。

创建敏捷构建不是为了赶时髦,它实际上是保证代码质量的重要因素。测试运行的更加频繁,开发人员的测试的价值就能直接转化为钱。并且,希望你们的公司能够在2006取得广泛的成功!

资源
Learn

Get products and technologies

Discuss

posted on 2006-12-01 00:30 Tin 阅读(1934) 评论(0)  编辑  收藏 所属分类: 开源

只有注册用户登录后才能发表评论。


网站导航: