随着软件项目的逐渐增大,软件测试在软件开发中的地位显得越来越重要。假如软件项目没有良好的测试流程,随着系统的增大,无论项目治理人员还是软件开发人员都会对项目的前景失去信心,甚至会对项目的目标产生分歧,因为长期以来没有对程序代码和系统设计进行有效的控制,很多问题都被暂时掩盖或逐渐演化成其他的问题。软件开发周期越长,就会使得问题进化的版本越多,最后造成的结果是“剪不断,理还乱”。
单元测试是整个测试流程中最基础的部分,它们要求程序员尽可能早地发现问题,并给予控制,这是其一。另外,假如集成测试出现问题,它们可以帮助诊断。这样就为在软件开发流程中建立高效的事件反应机制打下了坚实基础。
JUnit就是为java程序开发者实现单元测试提供一种框架,使得Java单元测试更规范有效,并且更有利于测试的集成。
JUnit的内部结构
JUnit的软件结构
JUnit 共有七个包,核心的包就是junit.framework 和junit.runner。Framework包负责整个测试对象的构架,Runner负责测试驱动。
JUnit的类结构
JUnit有四个重要的类:TestSuite、TestCase、TestResult、TestRunner。前三个类属于Framework包,后一个类在不同的环境下是不同的。这里使用的是文本测试环境,所以用的是 junit.textui.TestRunner。各个类的职责如下:
1.TestResult,负责收集TestCase所执行的结果,它将结果分为两类,客户可猜测的Failure和没有猜测的Error。同时负责将测试结果转发到TestListener(该接口由TestRunner继续)处理;
2.TestRunner,客户对象调用的起点,负责对整个测试流程的跟踪。能够显示返回的测试结果,并且报告测试的进度。
3.TestSuite, 负责包装和运行所有的TestCase。
4.TestCase, 客户测试类所要继续的类,负责测试时对客户类进行初始化,以及测试方法调用。
另外还有两个重要的接口:Test和TestListener。
1.Test, 包含两个方法:run() 和countTestCases(),它是对测试动作特征的提取。
2.TestListener, 包含四个方法:addError()、addFailure()、startTest()和endTest(),它是对测试结果的处理以及测试驱动过程的动作特征的提取。
下面给出的两个类图(篇幅有限,只显示主要部分)很好地阐明了类之间的关系,以及junit的设计目标(如图1)。测试案例的类采用Composite模式。这样,客户的测试对象就转变成一个“部分—整体”的层次结构。客户代码仅需要继续类TestCase,就可以轻松的与已有的其他对象组合使用,从而使得单元测试的集成更加方便。
图1 测试结构图
图2是测试跟踪类图。图2左边TestSuite包含了测试对象集合,右边包含了测试结果集。具体如何处理结果,以及包含哪些测试对象,并没有立即得出结论,而是尽量地延迟到具体实现的时候。例如,实现接口TestListener的JUnit中就含有:junit.awtui.TestRunner、junit.swingui. TestRunner、junit.ui.TestRunner等,甚至客户用自己的类实现TestListener,从而达到多样化的目的。
图2 测试跟踪图
从以上两个类图,可以了解JUnit对单元测试的基本思路,这个框架的核心就是结果集和案例集。
JUnit的实现流程
典型的使用JUnit的方法就是继续TestCase类,然后重载它的一些重要方法:setUp()、teardown()、runTest()(这些都是可选的),最后将这些客户对象组装到一个TestSuite对象中,交由 junit.textui.TestRunner.run (案例集) 驱动。下面分析案例集是如何运转的。
图3基本上阐述JUnit的测试流程架构。我们将从不同的角度来具体分析这个图。
图3 测试序列图
首先,从对象的创建上来分析。客户类负责创建Suite和aTestRunner。注重,类TestRunner含有一个静态函数Run(Test),它自创建本身,然后调用doRun()。客户类调用的一般是该函数,其代码如下:
static public void run(Test suite)
{
TestRunner aTestRunner= new TestRunner();//新建测试驱动
aTestRunner.doRun(suite, false);//用测试驱动运行测试集
}
Suite对象负责创建众多的测试案例,并将它们包容到本身。客户测试案例继续TestCase类,它将类,而不是对象传给Suite对象。Suite对象负责解析这些类、提取构造函数和待测试方法。以待测试方法为单位构造测试案例,测试案例的fName就是待测试方法名。测试结果集由aTestRunner创建。这似乎同先前阐述的类图有些矛盾,那里阐述了一个测试集可以包含很多个不同的测试驱动,似乎先创建结果集比较理想。显然,这里对测试结果的处理只采用了一种方式,所以这样做同样可行。
其次,从测试动作的执行上来分析,测试真正是从suite.run(result) 开始的。其代码如下:
public void run(TestResult result)
{
//从案例集中获得所有测试案例,分别执行
for (Enumeration e= tests(); e.hasMoreElements(); )
{
if (result.shouldStop() )
break;
Test test= (Test)e.nextElement();
runTest(test, result);
}
}
一旦测试案例开始执行,首先使用一个回调策略将自身交由Result。这样做的每一步测试,测试驱动aTest Runner都可以跟踪处理。这无形中建立了一个庞大的监视系统,随时都可以对所发生的事件给予不同等级的关注。
我们分析一下涉及到的动作行为的设计模式:
1. Template Method (模板方法)类行为模式,它的实质就是首先建立方法的骨架,而尽可能地将方法的具体实现向后推移。TestCase.runBare()就采用了这种模式,客户类均可以重载它的三个方法,这样使得测试的可伸缩性得到提高。
public void runBare() throws Throwable
{
setUp();
try {runTest();}
finally {tearDown();}
}
2. Command (命令)对象行为模式,其实质就是将动作封装为一个对象,而不关心动作的接收者。这样动作的接收者可以一直到动作具体执行时才需确定。接口Test就是一个Command集,使得不同类的不同测试方法可以通过同一种接口Test构造其框架结构。这样对测试的集成带来了很多方便。
JUnit的Exception的抛出机制
JUnit的异常层次分为三层:1.Failure,客户预知的测试失败,可以被Assert方法检测到;2. Error,客户测试的意外造成的;3.Systemerror, JUnit的线程死亡级异常,这种情况一般很少发生。JUnit的这三种异常在TestResult类的RunPRotected()方法得到很好体现。这里用Protectable接口封装了Test的执行方法,其实p.protect执行的就是test.runBare()。
public void runProtected(final Test test, Protectable p)
{
try {p.protect();}
catch (AssertionFailedError e)
{addFailure(test, e);}
catch (ThreadDeath e)
{rethrow e;}
catch (Throwable e)
{addError(test, e);}
}
代码首先检查是否是Assertion FailedError,然后判定是否是严重的ThreadDeath。这种异常必须Rethrow,才能保证线程真正的死亡,假如不是,说明它是一种意外。
前两种异常均保存在测试结果集中,等到整个测试完成,依次打印出来供客户参考。
实施JUnit的几点建议
从以上的分析中,可以了解JUnit的结构和流程,但是在实际应用JUnit时,有几点建议还需要说明,如下:
1. 客户类可以重载runTest(),它的缺省实现是调用方法名为fName的测试方法。假如客户不是使用TestSuite加载TestCase,就尤其需要对其重载,当然这种方式并不赞成使用,不利于集成。另外,setUp()和tearDown()的功能似乎与构造函数雷同,但假如测试案例之间具有类继续关系,采用构造函数初始化一些参数就会造成数据的混乱,不利于判定测试结果的有效性。
2. 待测试函数的调用顺序是不确定的,采用的数据结构是Vector()。假如需要有顺序关系,可以将它们组合到一起,然后用同一个测试方法。
3. 为了使测试结果清楚明了,程序中最好不要有打印输出,要么程序的打印输出与JUnit测试的打印输出不要用同一个数据源System.out。其实这是两种测试习惯,直接打印输出是较传统的,从测试动机上考虑它也是较随意的,并且结果需要人工观察。假如直接打印输出较多的话,观察者可能无法获得满足的结果。
此外,如何扩展这个测试框架呢? junit.extensions包给出了几点提示。我们可以使用junit.extensions. ActiveTest在不同的线程中运行一个测试实例。 对于要对测试案例添加新的功能可以采用Decorator模式,可以参考junit.extensions.TestDecorator以及它的子类junit.extensions.TestSetup、junit.extensions.RepeatedTest。这些仅仅提供了一些拓宽的思路,涉及到具体测试目标,还需进一步地挖掘。