分析 JUnit 框架源代码(理解 JUnit 测试框架实现原理和设计模式)
理解 JUnit 测试框架实现原理和设计模式
打印本页
将此页作为电子邮件发送
级别: 中级
何 正华?, 软件工程师, IBM
徐 晔?(yexu@cn.ibm.com?), 软件工程师, IBM, Software Group
2009 年 5 月 31 日
本文细致地描述了 JUnit 的代码实现,在展示代码流程 UML 图的基础上,详细分析 JUnit 的内部实现代码的功能与机制,并在涉及相关设计模式的地方结合代码予以说明。另外,分析过程还涉及 Reflection 等 Java 语言的高级特征。
概述
在 测试驱动的开发理念深入人心的今天,JUnit 在测试开发领域的核心地位日渐稳定。不仅 Eclipse 将 JUnit 作为默认的 IDE 集成组件,而且基于 JUnit 的各种测试框架也在业内被广泛应用,并获得了一致好评。目前介绍 JUnit 书籍文章虽然较多,但大多数是针对 JUnit 的具体应用实践,而对于 JUnit 本身的机制原理,只是停留在框架模块的较浅层次。
本文内容完全描述 JUnit 的细致代码实现,在展示代码流程 UML 图的基础上,详细分析 JUnit 的内部实现代码的功能与机制,并在涉及相关设计模式的地方结合代码予以说明。另外,分析过程还涉及 Reflection 等 Java 语言的高级特征。
本文的读者应该对 JUnit 的基本原理及各种设计模式有所了解,主要是面向从事 Java 相关技术的设计、开发与测试的人员。对于 C++,C# 程序员也有很好的借鉴作用。
Junit 简介
JUnit 的概念及用途
JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个开源的单元测试框架。它属于白盒测试,只要将待测类继承 TestCase 类,就可以利用 JUnit 的一系列机制进行便捷的自动测试了。
JUnit 的设计精简,易学易用,但是功能却非常强大,这归因于它内部完善的代码结构。 Erich Gamma 是著名的 GOF 之一,因此 JUnit 中深深渗透了扩展性优良的设计模式思想。 JUnit 提供的 API 既可以让您写出测试结果明确的可重用单元测试用例,也提供了单元测试用例成批运行的功能。在已经实现的框架中,用户可以选择三种方式来显示测试结果,并且 显示的方式本身也是可扩展的。
JUnit 基本原理
一个 JUnit 测试包含以下元素:
操作步骤:
将 B 通过命令行方式或图形界面选择方式传递给 R,R 自动运行测试,并显示结果。
JUnit 中的设计模式体现
设计模式(Design pattern)是一套被反复使用的、为众人所知的分类编目的代码设计经验总结。使用设计模式是为了可重用和扩展代码,增加代码的逻辑性和可靠性。设计模式的出现使代码的编制真正工程化,成为软件工程的基石。
GoF 的《设计模式》一书首次将设计模式提升到理论高度,并将之规范化。该书提出了 23 种基本设计模式,其后,在可复用面向对象软件的发展过程中,新的设计模式亦不断出现。
软 件框架通常定义了应用体系的整体结构类和对象的关系等等设计参数,以便于具体应用实现者能集中精力于应用本身的特定细节。因此,设计模式有助于对框架结构 的理解,成熟的框架通常使用了多种设计模式,JUnit 就是其中的优秀代表。设计模式是 JUnit 代码的精髓,没有设计模式,JUnit 代码无法达到在小代码量下的高扩展性。总体上看,有三种设计模式在 JUnit 设计中得到充分体现,分别为 Composite 模式、Command 模式以及 Observer 模式。
一个简单的 JUnit 程序实例
我 们首先用一个完整实例来说明 JUnit 的使用。由于本文的分析对象是 JUnit 本身的实现代码,因此测试类实例的简化无妨。本部分引入《 JUnit in Action 》中一个 HelloWorld 级别的测试实例,下文的整个分析会以该例子为基点,剖析 JUnit 源代码的内部流程。
待测试类如下:
该类只有一个 add 方法,即求两个浮点数之和返回。
下面介绍测试代码部分,本文以 JUnit3.8 为实验对象,JUnit4.0 架构类同。笔者对原书中的测试类做了一些修改,添加了一个必然失败的测试方法 testFail,目的是为了演示测试失败时的 JUnit 代码流程。
完整的测试类代码如下:
TestCalculator 扩展了 JUnit 的 TestCase 类,其中 testAdd 方法就是对 Calculator.add 方法的测试,它会在测试开始后由 JUnit 框架从类中提取出来运行。在 testAdd 中,Calculator 类被实例化,并输入测试参数 10 和 50,最后用 assertEquals 方法(基类 TestCase 提供)判断测试结果与预期是否相等。无论测试符合预期或不符合都会在测试工具
?TestRunner 中体现出来。
实例运行结果:
从运行结果中可见:testAdd 测试通过(未显示),而 testFail 测试失败。图形界面结果如下:
JUnit 源代码分析
JUnit 的完整生命周期分为 3 个阶段:初始化阶段、运行阶段和结果捕捉阶段。
初始化阶段(创建 Testcase 及 TestSuite)
初始化阶段作一些重要的初始化工作,它的入口点在 junit.textui.TestRunner 的 main 方法。该方法首先创建一个 TestRunner 实例?aTestRunner
?。之后 main 函数中主体工作函数为?TestResult r = aTestRunner.start(args) 。
它的函数构造体代码如下 :
我 们可以看出,Junit 首先对命令行参数进行解析:参数“ -wait ”(等待模式,测试完毕用户手动返回)、“ -c ”,“ -v ”(版本显示)。 -m 参数用于测试单个方法。这是 JUnit 提供给用户的一个非常轻便灵巧的测试功能,但是在一般情况下,用户会像本文前述那样在类名命令行参数,此时通过语句:
?TestSuite 的构造分两种情况 ( 如上图 ):
A:用户在测试类中通过声明 Suite() 方法自定义 TestSuite 。B:JUnit 自动判断并提取测试方法。JUnit 提供给用户两种构造测试集合的方法,用户既可以自行编码定义结构化的 TestCase 集合,也可以让 JUnit 框架自动创建测试集合,这种设计融合其它功能,让测试的构建、运行、反馈三个过程完全无缝一体化。
情况 A:
当 suite 方法在 test case 中定义时,JUnit 创建一个显式的 test suite,它利用 Java 语言的 Reflection 机制找出名为
?SUITE_METHODNAME?
的方法,也即 suite 方法:
当 suite 方法未在 test case 中定义时,JUnit 自动分析创建一个 test suite 。代码由 :
TestSuite 采用了Composite 设计模式。在该模式下,可以将 TestSuite 比作一棵树,树中可以包含子树(其它 TestSuite),也可以包含叶子 (TestCase),以此向下递归,直到底层全部落实到叶子为止。 JUnit 采用 Composite 模式维护测试集合的内部结构,使得所有分散的 TestCase 能够统一集中到一个或若干个 TestSuite 中,同类的 TestCase 在树中占据同等的位置,便于统一运行处理。另外,采用这种结构使测试集合获得了无限的扩充性,不需要重新构造测试集合,就能使新的 TestCase 不断加入到集合中。
在 TestSuite 类的代码中,可以找到:
?首先通过 String name= m.getName(); 利用 Refection API 获得 Method 对象 m 的方法名,用于特征判断。然后通过方法
?TestSuite 和 TestCase 都有一个fName
?实例变量,是在其后的测试运行及结果返回阶段中该 Test 的唯一标识,对 TestCase 来说,一般也是要测试的方法名。在?createTest
?方法中,测试方法被转化成一个 TestCase 实例,并通过:
该方法为测试的驱动运行部分,结构如下:
创建 TestResult 实例。将 junit.textui.TestRunner 的监听器 fPrinter 加入到 result 的监听器列表中。其 中,fPrinter 是 junit.textui.ResultPrinter 类的实例,该类提供了向控制台输出测试结果的一系列功能接口,输出的格式在类中定义。 ResultPrinter 类实现了 TestListener 接口,具体实现了 addError、addFailure、endTest 和 startTest 四个重要的方法,这种设计是 Observer 设计模式的体现,在 addListener 方法的代码中:
?Junit 通过?for (Enumeration e= tests(); e.hasMoreElements(); ){ …… }
?对 TestSuite 中的整个“树结构”递归遍历运行其中的节点和叶子。此处 JUnit 代码颇具说服力地说明了 Composite 模式的效力,run 接口方法的抽象具有重大意义,它实现了客户代码与复杂对象容器结构的解耦,让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来 处理复杂的对象容器。每次循环得到的节点 test,都同 result 一起传递给 runTest 方法,进行下一步更深入的运行。
这 里变量 P 指向一个实现了 Protectable 接口的匿名类的实例,Protectable 接口只有一个 protect 待实现方法。而 junit.framework.TestResult.runProtected(Test, Protectable) 方法的定义为:
?在 该方法中,最终的测试会传递给一个 runTest 方法执行,注意此处的 runTest 方法是无参的,注意与之前形似的方法区别。该方法中也出现了经典的 setUp 方法和 tearDown 方法,追溯代码可知它们的定义为空。用户可以覆盖两者,进行一些 fixture 的自定义和搭建。 ( 注意:tearDown 放在了 finally{} 中,在测试异常抛出后仍会被执行到,因此它是被保证运行的。 )
主体工作还是在 junit.framework.TestCase.runTest() 方法中 , 代码如下 :
该方法最根本的原理是:利用在图 13 中设定的 fName,借助 Reflection 机制,从 TestCase 中提取测试方法:
?测试结果捕捉阶段(返回 Fail 或 Error 并显示)
通过以下代码,我们可以看出失败由第一个 catch 子句捕获,并交由 addFailure 方法处理,而错误由第三个 catch 子句捕获,并交由 addError 方法处理。
JUnit 执行测试方法,并在测试结束后将失败和错误信息通知给所有的 test listener 。其中 addFailure、addError、endTest、startTest 是 TestListener 接口的四大方法,而 TestListener 涉及到 Observer 设计模式。
我们尝试看看 addFailure 方法的代码:
此 处代码将产生的失败对象加入到了 fFailures,可联系 图 2,此处的结果在程序退出时作为测试总体成功或失败的判断依据。而在 for 循环中,TestResult 对象循环遍历观察者(监听器)列表,通过调用相应的更新方法,更新所有的观察者信息,这部分代码也是整个 Observer 设计模式架构的重要部分。
根据以上描述,JUnit 采用 Observer 设计模式使得 TestResult 与众多测试结果监听器通过接口 TestListenner 达到松耦合,使 JUnit 可以支持不同的使用方式。目标对象(TestResult)不必关心有多少对象对自身注册,它只是根据列表通知所有观察者。因此,TestResult 不用更改自身代码,而轻易地支持了类似于 ResultPrinter 这种监听器的无限扩充。目前,已有文本界面、图形界面和 Eclipse 集成组件三种监听器,用户完全可以开发符合接口的更强大的监听器。
出于安全考虑,cloneListeners() 使用克隆机制取出监听器列表:
?这里并没有将错误信息输出,而只是输出了错误类型:“ F “。错误信息由图 14 中的:
总结
鉴于 JUnit 目前在测试领域的显赫地位,以及 JUnit 实现代码本身编写的简洁与艺术性,本文的详尽分析无论对于测试开发的实践运用、基于 JUnit 的高层框架的编写、以及设计模式与 Java 语言高级特征的学习都具有多面的重要意义。