【转】Junit 内部解密

Interface: Test 整个测试的的基础接口

Method 1: abstract int countTestCases()  这个方法主要是用来计算要运行的test case的数量的。

Method 2:abstract void run(TestResult result)  这个方法主要是用来执行一个测试用例并且在测试结果的实例中收集它的测试结果。

 

Class:TestCase

定义:abstract class TestCase extends Assert implements Test 继承了Assert类并且实现了Test接口。而且是个抽象类。

 

Class: TestSuite

定义:class TestSuite implements Test;  Test接口的实现类,和TestCase一样. 但是TestSuite是一系列testcase的集合,将所有要运行的testcase add到TestSuite。

                                          




 


观察一:TestCase 和 TestSuite类都实现了Test 接口。

好处一:由于当你给TestSuite增加一个对象时,实际上增加的是Test,而不只是个TestCase,这样我们就既可以在TestSuite里面增加另一个TestSuite,也可以加入一个TestCase。如果是TestCase,那么就会运行那个单独的测试;如果是TestSuite,那么就运行一组测试。

如何实现:那么是如何实现上面说的那个好处呢?看下TestSuite的一个addtest方法就明白了:

Public void addTest(Test test)     新增一个test到testsuite,直接使用fTests.add(test)来新增。

这里可以新增的入参是Test接口,而不是一个TestCase对象,所以只要实现了Test接口的实现类,包括TestCase和TestSuite,都可以不断的增加下去。这样就引出了好处二和设计模式。

 

好处二:当我们为自己的应用程序创建特殊的suite或组合出TestAll类非常容易。简单说明下TestAll类:仅仅包含了一个静态的suite方法,会注册需要定期执行的所有的Test对象(包括TestCase对象和TestSuite对象)。

Java设计模式:Composite模式

定义:把对象组合(composite)成树状结构来表示部分-整体层次关系,Composite模式可以让客户一致的对待单个对象和对象的组合。

 

我们来看下Junit的Test接口和TestCase和TestSuite类是如何实现Composite模式的:

先搞清楚Junit里面的部分-整体分别是什么,这里单个的TestCase可以看作是部分,把复合的TestCase(TestSuite)看作是整体,看下面的图:



 

 

我们可以看到这样的模式会带来另外一些好处:

简化了JUnit的代码  JUnit可以统一处理组合结构TestSuite和单个对象TestCase。使JUnit开发变得简单容易,因为不需要区分部分和整体的区别,不需要写一些充斥着if else的选择语句。

好处三:TestCase是个抽象实现类,而且继承了Assert类,这里为啥要继承Assert类呢?因为在一个testcase中是肯定需要用的Assert断言的,如果不继承Assert类,那么testcase方法中要写成Assert.assert(),不是很简洁,现在继承了Assert类后,我们就可以在继承了TestCase类的测试类中直接assert(),使测试代码看得非常简洁和清楚。

 

定义了TestCase对象和TestSuite的类层次结构  基本对象TestCase可以被组合成更复杂的组合对象TestSuite,而这些组合对象又可以被组合,如我们上个例子,这样不断地递归下去。客户代码中,任何使用基本对象的地方都方便的使用组合对象,大大简化系统维护和开发。

 

仔细看看Test接口的方法,它存在一个是countTestCases方法,它来统计这次测试有多少个TestCase,另外一个方法run。还有一个参数TestResult,它来统计测试结果。这里为啥存在一个run方法呢? 我们想想Junit在run testcase的时候,需要把这些testcase的组成打包后成为请求发送到Junit Framework,这样我们实现TestCase类的时候,可以自由的实现run方法去打包发送请求。这样我们在写测试用例时候,只需继承TestCase,来完成run方法即可,把测试结果记录到TestResult中,这样的做法就好引出另外的Java设计模式。

Java设计模式:Command模式

定义:将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求进行排队或记录请求日志...,Command告诉我们可以为一个操作生成一个对象并给出它的一个execute(执行)方法。

run()就是我们的Command模式的Excecute方法:



 

 

我们可以看到这样的模式会带来另外一些好处:

Command模式将实现请求的一方(TestCase开发)和调用一方(JUnit Fromwork)分离开。

Command模式可以将多个TestCase进行组合成一个复合命令,实际你将看到TestSuit就是它的复合命令。

 

      之前我们看到了Test接口里面的run方法有个TestResult的参数,不错,这个类就是用来收集测试结果的,是收集TestSuite的运行结果,所以一般情况下,一个TestSuite对应一个TestResult. TestResult存储了所有测试的详细情况,是通过还是失败。

如果是失败:Junit会创建一个TestFailure对象,并保存在TestResult中。



 

    这里知道了TestResult的作用,这种做法也会引入另外一个设计模式。

Java设计模式:Collecting Parameter模式

定义:当你需要从几个方法中收集结果时,你应当给方法增加一个参数,并传递一个会替你收集参数的对象。

这里TestResult类就是起到了这个作用。但是我们知道TestResult是收集很多运行的Test的运行结果,这里就需要对于这些运行结果进行管理,则TestResult类定义了如下相关的方法:

 

public synchronized void addError(Test test, Throwable t)    新增一个错误到ArrayList<TestFailure>。

public synchronized void addFailure(Test test, AssertionFailedError t)  新增一个失败到ArrayList<TestFailure>。

public synchronized void addListener(TestListener listener)   在一个test中注册一个监听器到ArrayList<TestListener>,这个监听器就是TestListener,实现类是TestRunner。

public synchronized void removeListener(TestListener listener)   从一个test中取消这个监听器。

private synchronized List<TestListener> cloneListeners()    克隆一批监听器。

 

    我们已经知道了TestResult的作用,那么TestListener的作用又是什么呢?在Run一个测试用例的时候有很多的结果,这时就由TestListener去观察这个运行的结果,并且负责报告这些运行信息。

 

    TestListener是个接口,一般由Test Runner,很多特定的Junit扩展也实现了这个接口,我们来看下这个接口里面定义了哪些方法:

public void addError(Test test, Throwable t);   发送错误的时候才被调用。

public void addFailure(Test test, AssertionFailedError t);     失败的时候才被调用。

public void endTest(Test test);     测试结束时被调用。

public void startTest(Test test);    测试开始时被调用。

 

    由于定义了这个TestListener接口和实现类TestRunner的作用都看到了,特别是给扩展Junit提供了新的实现类的方式,这样的做法就引出了一个设计模式。

Java设计模式:Observer模式

定义:在对象之间定义了一个一对多的依赖关系,这样当一个对象改变了状态,那么所有依赖于它的对象都会自动收到通知且更新。目前Junit框架的TestRunner就以TestListener的身份注册到TestResult。

 

      我们在写testcase的时候,都会用到Assert方法去check运行结果,这时候的Assert方法是继承了Junit的TestCase类,但是你如果还记得TestCase类的声明的话,那就是TestCase不仅仅实现了Test接口,而且也继承了Assert类,其实这些Assert方法就是Assert类中实现的。

 

     Junit的Assert类中总共有38个Assert方法,但很多都是不停的重载,其实就只有8个核心方法:

assertTure; assertFalse; assertEquals; assertNotEquals; assertNull; assertSame; assertNotSame; fail(让测试失败,并给出指定的信息)

一般要用到抛出message的都会用到fail方法。

 

我们以一个非常简单的TestCalculator类为例,只有一个测试方法:

Public class TestCalculator extends TestCase

{

       Public void testAdd()

       {

              Calculator calculator = new Calculator();

              Double result = calculator.add(10, 50);

              assertEquals(60, result);

       }

}

当我们使用Run in Junit 的时候,之前说到过的几个核心类之间是怎么运行的呢?我们先看下基本过程:

TestRunner启动界面框 —》创建一个TestSuite ––》创建一个TestResult—》执行testadd方法。

由于我们的TestCalculator测试类里面没有显式的suite方法,大部分情况下都是类似,这样的话,TestRunner创建了一个默认的TestSuite对象,看下图:



 



那么同时TestRunner还需要创建包含测试结果(成功,失败或出错)的TestResult对象,具体过程如下:



 

1.最开始的时候,TestRunner实例化了一个TestResult对象,在测试顺序执行的时候,这个对象将用于保存测试结果。

2.TestRunner向TestResult注册,就是add一个监听器,这样的话在执行测试过程中,TestRunner就可以收到各种事件,TestResult会广播如下方法:测试开始(startTest); 测试失败(addFailure); 遇到测试错误(addError); 测试结束(endTest)

3.知道了这些事件后,TestRunner就可以随着测试的进行而显示进度条了,并且在失败或错误的时候显示出来。

4.TestRunner通过调用TestSuite的run(TestResult)方法来开始测试

5.TestSuite为它所拥有的每个Test Case实例调用run(TestResult)方法。

6.Test Case使用传递给它的TestResult实例来调用其run(Test)方法,并把自身作为参数传递给run方法,这样TestResult 立马就可以用runBare()来回调它。

接着就是执行测试方法了:

这里需要说明的是对于每个TestCase都会调用runBare()方法,这里只有一个testAdd方法,所以只调用一次,请看下图:



 

1.runBare()方法将调用setUp, testAdd, teardown 方法,顺序执行。

2.如果调用3个方法的过程出现任何失败或错误,那么TestResult就会分别调用addFailure 和addError来通知它的所有Listener。

3.这样TestRunner就会收到这些错误或失败,也会罗列出这些错误,否则进度条就是绿色的,让你知道测试方法没有问题。

4.当tearDown方法执行完后,测试也就完成了,TestResult 会通过调用endTest把这个结果通告给所有的Listener。

 

 

    我们做使用Junit工具来做单页测试或接口测试时,需要注意一些问题,包括我们的编码规范,test规范,以及编写测试代码的策略,以下个人的总结。

1.为还没有实现的测试代码抛出一个异常。这就避免了该测试通过,而且会提醒你必须实现其代码。

2.一次只测试一个对象。单元测试一个重要的方面就是细粒度,它独立的检查你创建的每个对象,这样你就可以在问题发生的第一时间就把它们隔离起来。如果测试多于一个对象,那么你就无法预测到这些对象发生了改变时它们会如何相互影响的。

3.选择有意义的测试方法名。你应当能通过阅读方法名就可以理解要测试的是什么方法。一条好的规则就是一开始就遵守test_XX1_XX2的命名模式,其中XX1是待测方法名,XX2就是测试条件或目的。

4.在assert调用中解释失败原因。无论何时,只要你用到assertTrue,assertFalse,assertNull,assertNotNull方法,请记住要使用第一个参数是String类型的那个方法,这个参数让你可以提供一个有意义的文本描述,在断言失败的时候,Junit TestRunner 会显示这个描述,若不使用这个参数,那么当测试失败时就比较难找出失败原因了。

5.测试任何可能失败的事物。测试主执行路径很好,而且很需要做;但测试异常处理可能更重要。如果主执行路径出错,那么可能应用程序也无法工作。

6.让测试改善代码。编写单元测试常常有助于你写成更好的代码。理由很简单,testcase是你的代码的用户,只有在使用代码的时候才能发现代码的缺点,所以应当根据测试时发现的不便之处重构代码,使其更易于使用。TDD就类似,通过先编写测试,再从代码用户的角度来开发你的类。

7.让异常测试易读。把catch块中的异常变量命名为expected,这样就可以明确的告知读代码的人,这个异常是我们预期的,抛出异常才能让测试通过,在catch块中加入assertTrue(true)语句也进一步强调,这才是正确的执行路径。

8.同一个包,分离的目录。把所有测试类和待测类都放在同一个包中,但使用平行目录结构,为了可以访问保护型的方法,需要把测试和待测类放在同一个包中,为了简化文件管理,并让人清晰的分辨测试类和开发代码类,所有需要把测试放在一个单独的目录中。当然我们也可以使用一个test的包,进行遗漏所有开发包。这样也可以把test需要的resources文件也可也独立起来。

9.存在三种单元测试:逻辑单元测试,集成单元测试,功能单元测试。逻辑单元测试主要是检查代码逻辑性,通常只是针对单个方法,一般可以通过mock objects 和stub 来控制特定的方法的边界。 集成单元测试主要是在真实环境下的一个组件相互交互的测试。功能单元测试目的是为了确认响应的结果是否正确,这种单元测试更多的依赖于外部环境。



 

10.考察单元测试的覆盖率。一种方法就是数一下你的测试中用到了多少方法。使用Clover工具进行覆盖率统计,记住100%的测试覆盖并不能保证你的应用程序得到了100%的测试。

11.测试先行。在写任何代码之前,必须先写一个失败的测试。为啥是失败的测试呢,因为不是失败的测试,老是成功的测试,你就不会改进这些代码来使其更加具有维护性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值