(兰州大学信息科学与工程学院 甘肃 兰州 730000)
摘 要:JUnit发展到4.x版本后,已经不再单纯应用于单元测试,作为最早的开源自动化测试框架,一直被众多框架效仿借鉴,并拓展到更广泛的测试工作中。分析其内部的设计模式有助于我们理解自动化测试框架的主流设计思想,对于测试开发实践,具有指导性的帮助。
关键词:JUnit,自动化测试,测试框架,设计模式
1. 引言
JUnit 是一个开放源代码的Java测试框架,用于编写和运行可重复的测试。通过设置测试期望断言(Assertion),JUnit可以将测试结果共享并有组织地输出成可视化结果。从JUnit 3.x中的所有测试类需要继承TestCase编写用例脚本,到JUnit 4.x全面引入注解(Annotation)、提供了多种runner,并且不再使用反射获取测试类名称使得脚本命名更加灵活。JUnit 4.x 基本上是一个新框架,而不是旧框架的升级版本。新特性的引入使JUnit逐渐成为了最具竞争力的单元测试工具。目前,多数Java的开发环境都已经集成了JUnit作为单元测试的工具。
JUnit的设计开发基于Java语言,充分结合了Java编程语言的优越特性,并延续了高内聚、低耦合的设计原则。剖析JUnit的设计架构,分析其采用的设计模式不但有助于灵活掌握JUnit单元测试方法,更是为二次开发提供理论准备。
2. JUnit设计模式分析
JUnit的核心是围绕命令模式和组合模式设计的,当然同时使用了观察者模式,装饰模式,模版方法模式,适配器模式等。
2.1 命令模式
JUnit执行用例时序图如下:启动一次执行时,TestRunner先解析加载后,然后通过TestSuite、TestCase中的run方法继续解析成一个个Junit的测试方法(testcase),紧接着通过TestResult调用TestCase中的执行方法,通过TestCase中的runBare方法最终调用测试代码testXXX。
图2 JUnit设计模式-命令模式
可以看出TestRunner就是客户角色(Client),它通过doRun(Test suite)方法运行测试用例,并通过main方法TestResult result= createTestResult()构造请求者角色TestResult。
Test接口可以认为是命令模式中的命令角色(Command),其中的接口方法run(TestResult result)定义了需要执行的命令。
TestCase类可以看作是具体命令角色(Concrete Command),但是我们还需要自己通过继承TestCase类定义测试方法,这样的每一个测试方法都会被包装在一个TestCase实例中。
TestResult可以看作请求者角色(Invoker),它会通过自己的run(final TestCase test)方法运行测试并收集结果。
而接受者角色是最终执行命令的角色,在JUnit中,是通过TestCase中的runBare方法执行测试的,因此,TestCase既是具体命令角色,又是接受者角色。
(1)public void runBare() throws Throwable {
(2) ...
(3) try {
(4) runTest();
(5) }
(6) ....
(7)protected void runTest() throws Throwable {
(8)...
(9)}
2.2 组合模式
在面向对象的编程中,处理类似于树形结构或文件目录结构的任务时,通常将其抽象为组合模式。当测试达到一定规模的时候,必然要涉及到使用组合模式,用来管理和执行不同逻辑层级上的用例脚本。JUnit使用TestSuite组织TestCase。即TestSuite是一个测试集合,一个TestSuite可以包含一个或多个TestSuite或者TestCase。 TestSuite和TestCase都需要实现Test接口。
Test接口类:
(10)public abstract interface Test {
(11)public abstract int countTestCases();
(12)public abstract void run(TestResult paramTestResult);
(13)}
TestSuite类实现:
(1)public int countTestCases() {
(2)int count = 0;
(3)for (Test each : this.fTests) {
(4)count += each.countTestCases();
(5)}
(6)return count;
(7)}
(8)public void run(TestResult result) {
(9)for (Test each : this.fTests) {
(10)if (result.shouldStop()) {
(11)return;
(12)}
(13)runTest(each, result);
(14)}
(15)}
TestCase类实现:
(1)public int countTestCases() {
(2)return 1;
(3)}
(4)public void run(TestResult result) {
(5)result.run(this);
(6)}
作为Test的接口引用实例,JUnit的run方法通过多态引用Test变量,执行的是TestSuite还是TestCase,对于runner来说都是一样的。这大大简化了JUnit的脚本开发。
2.3 观察者模式
JUnit有两种Runner分别是:基于SWT、swing的UI Runner和控制台Runner。以控制台Runner。当一个case测试通过或失败后,无论是UI还是控制台都需要获取这类信息的更新动态,这个场景即可用观察者模式,也叫发布-订阅模式。被观察者称为主题,主题将更新的动态发布给自己所有的观察者,观察者对订阅的更新进行进一步处理。
图3 JUnit设计模式-观察者模式
JUnit省略了主题角色的抽象,只有具体的被观察主题TestResult,主题可以注册/注销观察者。每个主题可以绑定多个观察者,主题维护着一个观察者列表fListeners。
(1)protected List<TestListener> fListeners;
(2)public synchronized void addListener(TestListener listener) {
(3)this.fListeners.add(listener);
(4)}
(5)public synchronized void removeListener(TestListener listener) {
(6)this.fListeners.remove(listener);
(7)}
被观察主题TestResult的状态变更通过遍历观察者列表发布给主题所有的观察者。主题的发布方法命名正好与观察者信息更新方法名一致。
(1)public synchronized void addError(Test test, Throwable e) {
(2)this.fErrors.add(new TestFailure(test, e));
(3)for (TestListener each : cloneListeners())
(4)each.addError(test, e);
(5)}
(6)public synchronized void addFailure(Test test, AssertionFailedError e) {
(7)...
(8)}
(9)public void endTest(Test test) {
(10)for (TestListener each : cloneListeners())
(11)each.endTest(test);
(12)}
(13)public void startTest(Test test) {
(14)...
(15)}
JUnit的junit.framework包的TestListener即抽象观察者角色(Observer)定义了四个更新动作。
(1)public abstract interface TestListener {
(2)public abstract void addError(...);
(3)public abstract void addFailure(...);
(4)public abstract void endTest(...);
(5)public abstract void startTest(...);
(6)}
textui中的两个类为例,其中具体观察者角色(Concrete observer)ResultPrinter实现了观察者定义的方法,一旦有来自主题发布的状态即刻打印输出:
(1)public void addError(Test test, Throwable e) {
(2)getWriter().print("E");
(3)}
(4)public void addFailure(Test test, AssertionFailedError t) {
(5)getWriter().print("F");
(6)}
(7)public void endTest(Test test) {
(8)}
(9)public void startTest(Test test) {
(10)getWriter().print(".");
(11)if (this.fColumn++ >= 40) {
(12)getWriter().println();
(13)this.fColumn = 0;
(14)}
(15)}
junit.textui.TestRunner的doRun方法通过testResult.addListener(this.fPrinter)将观察者注册到具体主题上。
(1)public TestResult doRun(Test suite, boolean wait) {
(2)TestResult result = createTestResult();
(3)result.addListener(this.fPrinter);
(4)...
(5)}
2.4 装饰模式
在框架开发过程中,可能需要在原有类的核心职责和主要行为的基础上添加一些装饰功能,类的继承可以在原有的功能上进行扩展,但是这种扩展是静态的,一旦创建,不能修改。装饰模式将继承关系转化为关联关系。
命令模式实现了脚本执行的整个流程,但是如果要附加重复执行的功能,可以使用装饰模式。在package junit.extensions中,RepeatedTest,TestDecorator,TestSetup三个类就使用了装饰模式。
图4 JUnit设计模式-装饰模式
JUnit的装饰模式中涉及的角色分别是:
抽象构建角色(Component):给出一个抽象的接口,以规范准备接受附加功能的对象。相当于JUnit中的Test接口。
具体的构建角色(Concrete Component):定义一个将要接受附加功能的类。相当于JUnit中的TestCase类。
装饰角色(Decorator):持有一个抽象构建(Component)角色的引用,并定义一个与抽象构件一致的接口。即junit.extensions包中的TestDecorator。将构建角色(Component)的成员变量,利用装饰器的构造方法为成员变量赋值,通过重写成员变量的方法达到继承的效果。
(1)public class TestDecorator implements Test {
(2)protected Test fTest;
(3)public TestDecorator(Test test) {
(4)this.fTest = test;//利用装饰器的构造方法为成员变量赋值
(5)}
(6)public int countTestCases() {
(7)return this.fTest.countTestCases();
(8)}
(9) @override /通过重写成员变量的方法达到继承的效果
(10)public void run(TestResult result) {
(11)this.fTest.run(result);
(12)}
(13) ...
(14)}
具体的装饰角色(Concrete Decorator):负责给构建对象装饰上附加的功能。相当于junit.extensions包中的RepeatedTest,TestSetup,它们都需要去继承装饰角色TestDecorator。由于继承了装饰类,因此可以访问原来类的run方法,对run方法重写:
(1)public class RepeatedTest extends TestDecorator {
(2)private int fTimesRepeat;
(3) @Override //对run方法重写
(4)public void run(TestResult result) {
(5)for (int i = 0; i < this.fTimesRepeat; ++i) {
(6)if (result.shouldStop()) {
(7)return;
(8)}
(9)super.run(result);
(10)}
(11)}
(12) ...
(13)}
TestSetup是另一个具体装饰类,在重写run方法的同时,添加了新的功能
(1)public class TestSetup extends TestDecorator {
(2)public TestSetup(Test test) {
(3)super(test);
(4)}
(5) @override //重写run方法
(6)public void run(TestResult result) {
(7)Protectable p = new Protectable(result) {
(8)public void protect() throws Exception {
(9)TestSetup.this.setUp(); // 装饰了新方法setUp()
(10)TestSetup.this.basicRun(this.val$result);
(11)TestSetup.this.tearDown(); //// 装饰了新方法tearDown()
(12)}
(13)};
(14)result.runProtected(this, p);
(15)}
(16) // 增加了新方法setUp()
(17)protected void setUp() throws Exception {}
(18) // 增加了新方法tearDown()
(19)protected void tearDown() throws Exception {}
(20)}
3. JUnit 4.x新特性
在JUnit 3.x向JUnit 4.x演进的过程中,借鉴了其他测试框架的优点,并结合了Java 5语言的高级特性。使得JUnit不单单是自动化单元测试工具,其完善的设计结构具有良好的扩展性,可以适应更加多样化的测试需求。目前,JUnit 4.x版本主要的新特性包括:
1. 不再使用反射机制识别测试脚本文件,测试类无需继承Testcase类,脚本命名更加灵活;
2. 不再使用setUp()和tearDown()方法,通过@Before @After注解,将测试准备,和资源拆除放在对应的注解下。简单明了;
3. 提供多种场景下的Test runner:使用@RunWith注解即可指定特定的Runner完成组测试,参数化测试,忽略测试等;
4. 扩展了注解@Test的成员属性,通过给属性expected、timeout赋值,可以完成异常测试和超时测试;
5. 引入了 Hamcrest 匹配机制,提供了新的断言语法——assertThat,测试开发人员可以配合使用正则表达式或Java中的Matcher类库设计所需的假设条件;
6. 提供假设机制Assumption,可以跳过指定的测试失败点,继续执行剩余的测试;
7. 提供Theory机制,对具体的测试用例做参数集概括描述,以更好的适应TDD(测试驱动开发)的测试需求。
4. 结语
目前主流的自动化测试框架除了JUnit,TestNG也是最常见的测试框架之一。TestNG脱胎于JUnit的设计思想,并摆脱了JUnit 3.x的局限性,开创了基于注释的测试方法,以满足更广泛的测试需求,通过xml文件配置,支持命令行、ant等方式编译运行。本文通过分析JUnit的设计模式,实际上也涵盖了TestNG的基本设计要点。在遇到实际问题时,还需要结合理论,对相关技术灵活选型才能应对具体的测试场景。
参考文献
[1] JUnit Usage and idioms [EB/OL]. http://junit.org/junit4/
[2] mkyong. Java web development tutorials [EB/OL]. http://www.mkyong.com/.
[3] 周晓旭. 基于JUnit的自动化测试系统的设计与实现[D]. 西安: 西安电子科技大, 2012.
[4] 周雅慧. 基于JUnit的TDD自动化测试框架改进与实现[D]. 大连:大连理工大学, 2015. 11-12
[5] 戴建国, 郭理, 曹传东. JUnit框架剖析[J]. 计算机与数字工程, 2008, 8(36): 43-45
[6] (美) Tavhchiev, P等著, 王魁译. JUnit 实战:第2版[M]. 北京:人民邮电出版社, 2012.
[7] 程杰. 大话设计模式[M]. 北京:清华大学出版社, 2007.
论文作者:顾梦琪
论文发表刊物:《科技中国》2017年2期
论文发表时间:2017/5/2
标签:测试论文; 模式论文; 观察者论文; 方法论文; 角色论文; 框架论文; 主题论文; 《科技中国》2017年2期论文;