`
yxrs123456
  • 浏览: 1395 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

A Cook's Tour翻译

阅读更多

2011/7/18 21:51:14

JUnit 一个厨师的旅行

注:本文基于JUnit3.8.x

1,简介

   在先前的一篇文章(参考 Test Infected:Programmer Love Writing Tests,Java Report,July 1998,Volumn 3,Number 7)里,我们介绍了如何用一个简单的框架编写可重用的测试。在本文我们将讲述这个框架是如何构建的。

   我们仔细研究了JUnit框架的构建,发现里面有许多有价值的地方,讲述这些不同级别的知识点虽是一个艰难的任务,但我们会在探讨JUnit的构建过程中一一展示给大家。

   本文会先描述一下JUnit是做什么的,在随后的细节讲解中将会不断体现这些;接下来我们以设计模式的方式来讲述JUnit的设计与实现;最后,展望一下JUnit的发展。

2,目标

   JUnit的目标是什么?

   首先,我们假设一个程序如果没有被测试那么它不就不会运行,这要比一个程序员说:“我保证我的程序一直能运行起来”更可靠。

   从上述观点来看,开发人员仅仅写完代码测试通过还不算完活,还要写测试来证明程序是能运行的。但是我们都太忙了,都写那么多代码了,哪有闲功夫去写测试代码啊?程序员,伤不起啊!!!

   所以,首要任务就是开发一个能够写测试的框架。这个框架要易学易用,消除冗余代码。

   如果所有的测试都这样,那我们只需要在调试器中写些表达式就行了。但这远远不够,因为一个程序员写的代码不能保证当集成了别的代码后它还能正确运行,也不能保证以后都一直正确运行下去。

   因此,这个测试框架的第二个设计目标就是能够保留测试代码,这样,除程序的原作者外其它程序员也可执行这些测试并得到结果,还可以将不同人写的测试代码集成起来运行。

   最后,在这个测试框架中,我们还应该利用已有的测试开发新的测试;当一个程序的初始化任务比较麻烦,这个框架还应该在不同的测试中重用这些初始化任务。大体上就这些了。

3JUnit的设计实现

   JUnit的设计思想最初源于“模式生成架构”(Patterns Generate ArchitecturesKent Beck and Ralph Johnson,ECOOP 94)一文,就是从零开始,不断的运用设计模式,直到将这个框架构造起来。本文将展示要解决什么架构问题,然后讲用什么模式去解决它,最后讲这些模式是如何被应用到JUnit中的。

 3.1,开始---TestCase(测试用例)

   我们先用一个对象来表示测试用例。开发人员通常用多种不同方式来实现测试用例,比如:打印语句,调试语句,测试脚本。

   测试在开发人员脑海中常常是模糊的,如果想容易的运行测试,最好将测试做为对象,这样就将测试具体化了,并且能这些测试保留下来,同时,开发人员也习惯于对象编程,更适合开发。

   命令模式-The Command Pattern(参考Gamma,E,et al,Design Patterns:Elements of Reusable Objected-Oriented Software,Addsion-Wesley,Reading,MA,1995)很符合我们需求,引用下命令模式的定义:“将一个对象封装为对象用来查询或记录请求...”,命令模式创建一个带“execute”方法的对象去执行任务,下面是TestCase的定义:

   public abstract class TestCase implements Test{

       ...

   }

   我们通过继承机制想重用TestCase,所以我们将TestCase声明为“public abstract”,先不要管它实现了“Test”接口,暂时先将它看做一个孤立的类。每个TestCase通过一个属性“name”来区分,这样当某个测试失败,就能找出具体失败的测试。

   public abstract class TestCase implements Test{

           private final String fName;

           public TestCase(String name){

               this.fName = name;

           }

           public abstract void run();

           ...

   }

   我们通过图来展示JUnit的架构,从中可以看到它的发展进程。图中的注释很简单,仅仅通过灰色的图标来表示用到的模式。如果在一个类在模式中扮演的角色很明显,那么只显示下模式的名字就行了,否则补充这个类的名称,避免了了图示的混乱,这种方式首先在被用在Gamma,E,Applying Design Patterns in Java,in Java Gems,SIGS Reference Library,1997这本书中,图1展示了TestCasae用到的这种注释方式,因为这个类的角色很清晰,所以仅标明了模式的名称。

 

1TestCase应用命令模式

3.2 填充run()方法

   接下来我们就要写测试代码和初始化代码,为了重用代码我们将TestCase声明为抽象类,但是,如果仅仅提供一个空的抽象类,那显然不能满足JUnit的第一个目标:编写简单容易的测试。

   幸运的是:所有的测试采用同一种结构-做一些初始化工作:运行测试代码,检测测试结果,处理结束任务。也就是说:所有的测试都互不影响(每个测试的初始化任务和测试结果不会影响其它测试),这保证了测试代码的最大价值。

   模板模式很好的解决了这个问题,引用模板模式的定义:“定义一个算法的框架,然后在子类中实现某些步骤。模板方法在不改变算法的框架前提下可以在子类中改变它的某些具体实现。”这样开发人员就可以单独地写初始化代码(set up),关闭代码(tear down),测试代码,不管如何写,所有测试的执行顺序都是一样的。下面是模板模式:

        public void run(){

                setUp();

                runTest();

                tearDown();

        }

   这些方法的默认实现是空方法:

        protected void runTest(){}

        protected void setUp(){}

        protected void tearDown(){}

    因为setUp()tearDown()方法可以被子类覆盖,所以定义为“protected”类型,图二描述了模板模式。

 

2TestCase.run()应用了模板模式

3.3 TestResult-记录测试结果

    当一个测试用例运行完,你有注意过它的执行结果吗?当然了,我们测试是为了保证代码正确运行,但测试完了呢?我们常常需要记录测试结果。

    如果测试成功和失败的可能性相等,或者当仅仅运行一个测试的时候,我们可以在测试用例(TestCase)中设置一个属性(flag)来记录运行结果。遗憾的是:我们并不能确定测试成功率和失败率,因此,我们主要记录测试失败的情况,对于测试成功的情况仅概述一下。

    Smalltalk Best Practice Patternssee Beck,K,Smalltalk Best Practice Patterns,Prentice Hall,1996)书中有一个叫做“收集参数”的模式挺符合要求的,这个模式指出:如果需要在若干个方法中收集结果,那么应该在这些方法中传递一个对象作为参数用来收集结果。在JUnit中,TestResult这个对象用来记录运行结果。

        public class TestResult extends Ojbect{

                protected int fRunTests;

                public TestResult(){

                        fRunTests = 0;

                }

        }

    上面这个简单的TestResult仅仅记录了测试的个数,为了应用TestResult,还得把它作为TestCase.run()方法的一个参数,这样TestResult则能判断一个测试是否在运行:

        public void run(TestResult result){

                result.startTest(this);

                setUp();

                runTest();

                tearDown();

        }

    当然了,TestResult记录测试个数的方法如下:

        public synchronized void startTest(Test test){

                fRunTests++;

        }

    startTest方法声明为synchronized是为保证TestResult是线程安全的,并且为了保持TestCase的扩展性,在TestCase中还有一个无参的run()方法,这样就可以自定义TestResult

        public TestResult run(){

                TestResult result = createResult();

                run(result);

                return result;

        }

        protected TestResult createResult(){

                return new TestResult();

        }

     3展示了“收集参数”模式:

 

3TestResult应用了收集参数模式

     如果测试都能正确运行,那就没必要写测试了,我们更关注测试失败的情况,特别是不可预料的失败;我们也可以编写可预期的失败测试,比如计算一个错误的值,或者其它不可思议的错误如数组越界,但不管前边的测试是怎么失败了,我们希望后面的测试依旧可执行。

     JUnit中区分失败(failures)和错误(errors)。失败(failure)是可预测的,用assertion(断言)来检测,失败会被标注为AssertionFailedError,但错误是不可预测的比如ArrayIndexOutOfBoundsException(数组越界异常);为了区分失败(failure)和错误(error),在JUnit中,失败的catch代码块在前(如下代码注释1位置),而其它的异常catch块在后(如下代码注释2位置),这样确保了后边的测试继续执行...

        public void run(TestResult result){

                result.startTest(this);

                setUp();

                try{

                        runTest();

                }catch(AssertionFailedError e){//1

                    result.addFailure(this,e);   

                }catch(Throwable e){//2

                        result.addError(this,e);

                }finally{

                        tearDown();

                }

        }

     TestCase用断言方法判断AssertionFailedError,还有其它许多断言失败的方法,下面是最简单的一个:

                protected void assertTrue(){

                        if(!condition){

                                throw new AssertionFailedError();

                        }

                }

     AssertionFailedError类继承自Error类,在我们写的测试代码中并不能捕获不到AssertionFailedError,在TestCase.run()方法中才可以捕获到。

                public class AssertionFailedError extends Error{

                        public AssertionFailedError(){}

                }

     TestResult类中收集错误和失败的方法如下:

                public synchronized void addError(Test test,Throwable t){

                        fErrors.addElement(new TestFailure(test,t));

                }

                public synchronized void addFailure(Test test,Throwable t){

                        fFailures.addElement(new TestFailure(test,t));

                }

     TestFailure类是一个辅助类,它关联了失败的测试类和异常。

                public class TestFailure extends Ojbect{

                        protected Test fFailedTest;

                        protected Throwable fThrownException;

                }

     “收集参数”模式最初的定义要求把要收集的参数传递给所有的方法,但是这样的话,每个测试方法都得有一个参数TestResult,对测试的方法造成了“侵入性”,利用异常(Exception)可以避免对测试方法的“侵入”,一个测试方法或者一个调用测试方法的辅助方法,不用去关注TestResult就可以抛异常,下面是MoneyTest Suite(这个测试类在JUnit自带的测试类中)中一个简单的方法很好说明了上述问题:

                public void testMoneyEquals(){

                        assertTure(!f12CHF.equals(null));

                        assertEqueals(f12CHF,f12CHF);

                        assertEqueals(f12CHF,new Money(12,"CHF"));

                        assertTrue(!f12CHF.equals(f14CHF));

                }

      JUnit中有多种TestResult的实现类,默认的实现可以计算失败(failures)和错误(errors)的个数,收集测试结果;TextTestResult以文本的方式显示测试结果;UITestResult用在图形测试界面,可以在图形上展示测试结果的状态。当然了,用户也可以自定义TestResult,比如说写一个HTMLTestResult用网页的形式来记录测试结果。

3.4 不写多余的子类 TestCase

      我们用命令来表示测试(Test),通过调用一个简单的方法(在TestCase中是run()方法)执行命令,用Test这个简单的接口可以调用不同的实现。

      我们需要一个通用的接口(Test)去运行测试用例,但是,所有的测试用例可能在同一个类中以不同的方法实现,这样避免编写过多的测试类;一个测试类可能实现了许多不同的方法,每个方法都代表一个测试用例,每个测试用例都有一个描述性的名字如testMoneyEqualstestMoneyAdd。这些测试用例并不需要遵循命令接口,可以用不同的测试方法去调用命令接口(Test)的不同实现类,因此下一个要解决的问题就是让测试用例看起来一样。

      思考一下这个问题,想到了适配器模式(Adapter),适配器模式定义如下:“将一个接口封装到用户自己的接口中”,这符合我们的要求,可以用不同的方法解决我们的问题,其中一种方法就是:用子类去匹配Test接口,例如,为了匹配testMoneyEqualsrunTest,通过一个MoneyTest的子类覆盖runTest()方法,用这个runTest()方法去调用testMoneyEquals

                public class TestMoneyEquals extends MoneyTest{

                        public TestMoneyEquals(){super("testMoneyEquals");}

                        protected void runTest(){testMoneyEquals();}

                }

      采用继承的这种方式要求我们为每一个测试用例都写一个子类,这给我们带来额外的工作负担,显然这不符合JUnit的一个目标--当我们新增一个测试用例的时候代码尽可能的简单,此外,为每一个测试方法写一个子类导致了类数量的增加,为了一个简单的方法专门写一个类是不值得的,并且给这个类起一个有意义的名字也不是件容易的事。

      还好Java提供了匿名内部来解决命名问题,用匿名内部类可以不用新建一个类就应用适配器模式。

                TestCase test = new MoneyTest("testMoneyEquals"){

                        protected void runTest(){testMoneyEqueals();}

                }

      编写匿名内部类的方式要比写子类方便的多,也需要编译时检查类型,同时也增加了开发人员的负担。对于同一个可替换的行为接口(Pluggable behavior)下,不同的实现类表现不同的行为这个问题,SmallTalk Best Practise Patterns提供了另一个解决方法:编写一个可被参数化的类执行不同的逻辑,这样就可以不用写子类了。

      Pluggable behavior的最简单形式就是Pluggable SelectorPluggable Selector用一个实例变量存储一个SmallTalk的方法,这个思想不局限于SmallTalk语言,同样适用于Java,在Java中没有方法选择器的概念,但是利用Java提供的反应机制可以通过方法名来调用一个方法,用这个特征就可以在Java中实现一个Pluggable Selector,通常我们不使用反射,但在这里构建一个基础的框架,我们要用到它。

      用户可以选择使用Pluggable Selector的方式或者写一个匿名内部类,默认情况下,JUnit提供了一个Pluggable Selector来实现runTest()方法,这样每一个测试用例的名字就必须和测试方法的名字一致,下面我们反射调用一个方法:先获取Method对象,一旦获取到就给它传递参数调用它,因为测试方法是无参的,所以传递一个空的参数数组:

                protected void runTest() throws Throwable{

                        Method runMethod = null;

                        try{

                                runMethod = getClass().getMethod(fName,new Class[0]);

                        }catch(NoSuchMethodException e){

                                assertTrue("Method \"" + fName + "\" not found",false);

                        }

                        try{

                                runMethod.invoke(this,new Class[0]);

                        }//catch InvocationTargetException and IllegalAccessException

                }

      JDK1.1中反射机制只能获取public方法,因此不得不将测试方法声明为public类型,否则将出现NoSuchMethodException

      下面图4展示了适配器模式和Pluggable Selector

 

4TestCase可应用茂名内部类或Pleggable Selector模式

3.5 有了TestSuite就不用担心多个测试了

      我们编写大量的测试确保应用的正确运行,目前JUnit可以运行一个测试用例并在TestResult中记录测试结果,如果有很多测试,那就得想法扩展JUnit,假设用户不用担心是运行一个测试还是运行多个测试,那么这个问题就很轻松的解决了,组合模式(Composite)应运而生,引用组合模式的定义:“将多个对象组织成树型结构来表示部分和整体的关系,在用户眼中,单个对象和多个对象是一样的。”部分和整体的关系很有趣,在这我们支持多个测试用例。

      组合模式包含下面三种角色:

        Component(组件):定义和测试类的接口;

        Composite(枝干):实现Component接口,表示多个测试用例的集合;

        Leaf(叶子节点):实现Component接口,表示单个的测试用例;

      根据组合模式的定义,我们要声明一个抽象类,然后让LeafComposite继承它,这个抽象类主要就是为了声明一个接口,当在Java中应用组合模式的话,我们更喜欢用接口而不是抽象类,使用接口可以不用将测试功能交给某个特定的基类,我们只需要将子类继承这个接口,下面来看看这个接口:

        public interface Test{

                public abstract void run(TestResult result);

        }

      JUnitTestCase实现了Test接口,就是组合模式中的叫叶子节点(Leaf)。那枝干(Composite)是哪个类呢?是TestSuite类,它用Vector(集合)来存储子节点:

        public class TestSuite implements Test{

                private Vector fTests = new Vector();

        }

      TestSuiterun()方法其实是交给它的子节点来实现的:

        public void run(TestResult result){

                for(Enumeration e = fTests.elements();e.hasMoreElements();){

                        Test test = (Test)e.nextElement();

                        test.run(result);

                }

        }

 

5TestSuite应用了组合模式

      TestSuiteaddTest()方法可以添加新的测试用例:

        public void addTest(Test test){

                fTests.addElement(test);

        }

      观察以上代码是如何依赖Test接口的,因为TestCaseTestSuite都实现了Test接口,所以通过递归我们可以无限的组合测试用例(TestCase)和测试套件(TestSuite),创建一个测试套件(TestSuite)可以运行我们所有的测试。下面是一个测试套件(TestSuite)的例子:

        public static Test suite(){

                TestSuite suite = new TestSuite();

                suite.addTest(new MoneyTest("testMoneyEqueals"));

                suite.addTest(new MoneyTest("testSimpleAdd"));

        }

      上面的例子很好的应用了TestSuite,但我们要手动地给TestSuite添加测试用例,JUnit早期的设计者不提倡我们这么做,因为每当你想要新增一个测试用例的时候,你都得把它添加到一个static suite()方法中,否则这个测试用例就不会执行;还好TestSuite有一个构造函数给我们提供了方便,我们只需给这个构建函数传递测试用例的类型就可以了,然后通过反射机制就可以找到这个测试方法,自动将它添加到TestSuite中,这个时候我们的必需遵循一个约定:测试方法以“test”开头且不能有参数;如果用这种方式上例的代码可以这么写:

        public static Test suite(){

                return new TestSuite(MoneyTest.class);

        }

      如果仅仅想运行一组测试的其中一部分的,上一种方式还是可行的。

3.6 总结

      至此,对于JUnit的讲解结束了,下图是JUnit中用到的设计模式:

 

6JUnit模式概览

      特别要注意的是TestCase类,它是JUnit框架的核心类,用到了4个设计模式,与其它类关联性很强。

      让我们从另一个角度去看一下JUnit中用到的设计模式,下面是一个框架进化图,可以看到每个模式对构建框架起到的作用,比如:通过命令模式构建了TestCase类,模板模式构建了run()方法等等。(下面的进化图是通过图6转化的,只不过是去掉了图6中的文字)

 

7JUnit模式进化图

      上图我们要注意一点:当图片中到组合类这一步的时候就变得复杂了,这是组合模式功能强大的一个副作用吧,因此应用组合模式时要谨慎。

4.结论

      设计模式:

           当我们在开发一相框架或者向他人讲解框架时,联系设计模式讲解是一种很有效的方式,如果你也喜欢这种方式,那么可以尝试一下。

      设计模式的密集度:

           JUnit中核心TestCase类应用了多个设计模式(即TestCase模式密集度高),使用起来比较方便,但修改起来比较麻烦,一般成熟的框架应用模式的密集度都较高,相反,不成熟的框架模式密集度就低了,你如果发现你的程序中存在这样的问题,那么就尝试着提高你程序的模式密集度,这样你就体会到模式的威力了。

      JUnit测试自身:

      一旦我们写好测试用例然后运行它,接着就可以看到JUnit报告的测试结果(有成功success,有失败failure,也有错误error)符合我们的期望,假如我们想再扩展框架,这是很有用的,JUnit最大的好处就是它可以测试自身运行是否正确。

      选择你自己喜欢的测试类

      开发框架时为了使框架发挥它最大的价值,尽可能多让框架的包含一些特性,但同时又希望用户尽可能的少学习框架就能够应用,框架特性越少用起来就越容易,那么用户也更容易接受,JUnit的设计就遵从这一理念,JUnit只实现了那些运行测试必要的代码,不同的测试可以互不影响,测试可以自动运行;JUnit还提供了一个扩展包,里面添加了一些新特性,其中一个代表性的类就是TestDecorator,它可以让我们在测试前后加自己的代码。

      框架的作者检查代码:

      JUnit的作者花在检查代码上的时间要比写框架的时间多得多,每当新增一个功能就要花同样的时间去删除重复的代码,不断地尝试设计,新增功能,当然作者也在不断的改进代码,获益良多。

      JUnit的最新版本可以从http://www.junit.org下载。

5,鸣谢

      感谢John Vissides,Ralph Johnson,Nick Edgar斧正。

参考文献:

http://junit.sourceforge.net/doc/cookstour/cookstour.htm

http://wjason.iteye.com/blog/579516

注:读了英文原著,利用业余时间翻译出来,与大家共享,仅供参考。

译者邮箱:mestudying@sina.com 欢迎一起探讨。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics