内容
简介: 在采用测试驱动开发的项目中,有一个经常困扰开发者的问题是:当存在大量的测试用例时,一次运行完所有的测试用例要花费很长的时间,采用
TestSuite 来组织测试用例的方式缺乏灵活性,通常它的组织结构大体和 Java Package/Class
的组织结构类似,不能和当前实现的业务需求完全相关。本文将通过扩展 JUnit4 来实现一种可以更加高效灵活地组织和运行测试用例的解决方案,促进测试驱动开发实践更有效地进行。
在敏捷开发中,为了提高软件开发的效率和质量,测试驱动开发实践已经被广泛使用。在测试驱动开发的项目中,随着项目开发不断地深入,积累的测试用例会越来越多。测试驱动开发的一个最佳实践是随时运行测试用例,保证任何时候测试用例都能成功执行,从而保证项目的代码是可工作的。当测试用例数量很多时,一次运行所有测试用例所消耗的时间可能会很长,导致运行测试用例的成本很高。所以在实际敏捷开发中,如何组织、运行测试用例以促进测试驱动开发成为一个值得探究的问题。
JUnit 是 Java 开发中最常用的单元测试工具。在 JUnit3 用 TestSuite 来显式地组织想要运行的
TestCase,通常 TestSuite 的组织大体上和 Java Package/Class 的组织类似,但这样并不能和当前正在实现的业务需求完全相关,显得比较笨拙,比如说要运行某个子模块下所有的
TestCase,或者运行跟某个具体功能相关的 TestCase,涉及到的 TestCase 数量可能较多,采用定义
TestSuite 的方式一个个地添加 TestCase 很低效并且繁琐。在 JUnit4 中同样只能显式地组织要运行的
TestCase。
怎么样解决这些问题,新发布的 JUnit4 提供了开发人员扩展的机制,可以通过对 JUnit 进行扩展来提供一种解决的方法。
JUnit4 引入了 Java5 的 Annotation 机制,来简化原有的使用方法。测试用例不再需要继承
TestCase 类,TestSuite 类也取消了,改用 @Suite.SuiteClasses 来组织
TestCase。但是这种还是通过显示指定 TestCase 来组织运行的结构,不能解决上述的问题。关于
JUnit4 的新特性具体可以参考 developerworks 的文章。
JUnit4 的实现代码中提供了 Runner 类来封装测试用例的执行。它本身提供了 Runner 的多种实现,比如
ParentRunner 类、Suite 类,BlockJUnit4ClassRunner 类。我们可以充分利用
JUnit4 提供的已有设施来对它进行扩展,实现我们期望的功能。
首先我们来分析一下 JUnit4 在运行一个测试用例时,它内部的核心类是如何工作的。图 1 展示了 JUnit4
运行测试用例时,核心类之间的调用关系。
图 1. JUnit4 核心类之间的调用关系
(查看图 1 的
清晰版本。)
在 JUnit4 中,Runner 类定义了运行测试用例的接口,默认提供的 Runner 实现类有 Suite、BlockJUnit4ClassRunner、Parameterized
等等。Suite 类相当于 JUnit3 中的 TestSuite,BlockJUnit4ClassRunner
用来执行单个的测试用例。BlockJUnit4ClassRunner 关联了一个 TestClass 类,TestClass
封装了测试用例的 Class 元数据,可以访问到测试用例的 method、annotation 等。FrameworkMethod
封装了测试用例方法的元数据。从下图中我们可以看到这些类的关系。
图 2. JUnit4 核心类
通过扩展 JUnit4,我们一方面可以无缝地利用 JUnit4 执行测试用例的能力,另一方面可以将我们定义的一些业务功能添加到
JUnit 中来。我们将自定义一套与运行测试用例相关的业务属性的 Annotation 库,定义自己的过滤器,扩展
JUnit 类的 Runner,从而实现定制化的测试用例的执行。
下面我们来描述一下对 JUnit4 扩展的实现。扩展包括 4 个模块,Annotation 定义、用户查询条件封装、过滤器定义、核心类定义。
JUnit4 用 Annotation 来定义测试用例运行时的属性。我们可以定义自己的 Annotation
库。通过定义出具体项目中和执行测试用例相关的属性元数据, 比如某个模块,某个特性,将这些属性通过 Annotation
附加到测试用例中,在扩展的 Runner 中利用过滤器对测试用例进行过滤,从而执行目标测试用例。
根据实际项目中的开发经验,我们大体抽象出了如下的几种 Annotation, 可以映射到我们项目的业务功能划分上;
表 1. 扩展的 Annotation 的具体用法
名称 |
参数 |
作用域 |
Product |
字符串参数,指定要测试的产品项目名称 |
类 |
Release |
字符串参数,指定具体的 Release 编号 |
类、方法 |
Component |
字符串参数,指定子模块、子系统 |
类 |
Feature |
字符串参数,指定某个具体的功能、需求 |
类、方法 |
Defect |
字符串参数,指定测试中发现的 Defect 的编号 |
类、方法 |
UseCaseID |
字符串参数,指定 UseCase 的编号 |
类、方法 |
当我们想要运行所有和 Feature 相关的测试用例时,我们只要指定执行条件,就可以只运行那部分测试用例,而不会去运行全部的测试用例。这种方法从业务的角度来看,更加具有针对性,而且简洁快速,比用传统的通过
TestSuite 指定测试用例的方式更加适合测试驱动开发的场景。下面给出 Feature Annotation
和 Release Annotation 的定义作为示例。
清单 1:Feature Annotation 的定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Feature {
String value();
}
|
清单 2:Release Annotation 的定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Release {
String value();
} |
接下来是封装用户输入的执行条件。在这里我们约定用户输入的执行条件的格式是:“条件 A = 值 A,条件
B = 值 B”。比如用户想执行 Release A 中的跟 Feature B 相关的测试用例和方法,那么用户的输入条件可以定义为“Release=A,Feature=B”。下图是封装用户输入的类的结构:
图 3. 封装用户输入的执行条件的类
过滤器是用来根据用户输入,对目标测试用例和测试方法进行过滤,从而找到符合条件的测试用例方法。用户输入的每个条件都会生出相应的一个过滤器,只有测试用例满足过滤器链中所有的过滤条件,测试用例才能被执行。下面的清单展示了过滤器接口的定义和过滤器工厂的核心实现。过滤器工厂会根据用户输入的条件来创建对应的过滤器。
清单 3 . Filter 接口的定义
public interface Filter {
public boolean shouldRun(IntentionObject object);
} |
清单 4 . FilterFactory 的部分实现
public class FilterFactory {
public static Map<Class<?>, List<Filter>> createFilters(String intention)
throws ClassNotFoundException{
Map<Class<?>, List<Filter>> filters = new HashMap<Class<?>, List<Filter>>();
String[] splits = intention.split(ExtensionConstant.REGEX_COMMA);
for(String split : splits){
String[] pair = split.split(ExtensionConstant.REGEX_EQUAL);
if(pair != null && pair.length == 2){
Filter filter = createFilter(pair[0],pair[1]);
String annotationType = ExtensionConstant.ANNOTATION_PREFIX + pair[0];
Class<?> annotation = Class.forName(annotationType);
List<Filter> filterList = null;
if(filters.containsKey(annotation)){
filterList = filters.get(annotation);
}else{
filterList = new ArrayList<Filter>();
}
filterList.add(filter);
filters.put(annotation, filterList);
}
}
return filters;
}
………………
} |
核心类模块中的类是对 JUnit4 中的类的扩展,从下图中可以看到两者的继承关系:
图 4. 核心扩展类和 JUnit4 中类的继承关系
Request 类是 JUnit4 中用来表示一次测试用例请求的抽象概念。它是一次测试用例执行的发起点。RunerBuilder
会根据测试用例来创建相应的 Runner 实现类。BlockJUnit4ClassRunner 是 JUnit4
中用来执行单独一个测试用例的 Runner 实现类。我们通过扩展它,来获得 JUnit 执行测试用例的能力,同时在
ExtensionRunner 中调用过滤器对测试用例方法进行过滤,从而根据我们定义的业务规则来执行测试用例。Result
类是 JUnit4 中用来封装测试用例执行结果的类,我们对它进行了扩展,来格式化测试用例执行结果的输出。下面给出
ExtensionRunner 的部分实现。
清单 5. ExtensionRunner 部分实现
public class ExtensionRunner extends BlockJUnit4ClassRunner {
private Map<Class<?>, List<Filter>> filtersForAnnotation;
public ExtensionRunner(Class<?> klass, String intention)
throws InitializationError, ClassNotFoundException {
super(klass);
filtersForAnnotation = FilterFactory.createFilters(intention);
}
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}
protected void runChildren(final RunNotifier notifier) {
for (final FrameworkMethod each : getFilteredChildren()) {
runChild(each, notifier);
}
}
protected List<FrameworkMethod> getFilteredChildren() {
ArrayList<FrameworkMethod> filtered = new ArrayList<FrameworkMethod>();
for (FrameworkMethod each : getChildren()) {
if (shouldRun(each)) {
filtered.add(each);
}
}
return filtered;
}
protected boolean shouldRun(FrameworkMethod method) {
List<Boolean> result = new ArrayList<Boolean>();
Annotation[] classAnnotations = method.getAnnotations();
Map<Class<?>,Annotation> methodAnnotationMap =
getAnnotaionTypeMap(classAnnotations);
Set<Class<?>> annotationKeys = filtersForAnnotation.keySet();
for(Class<?> annotationKey : annotationKeys ){
if(methodAnnotationMap.containsKey(annotationKey)){
List<Filter> filters = filtersForAnnotation.get(annotationKey);
if (filters != null) {
for (Filter filter : filters) {
if (filter != null
&& filter.shouldRun(
IntentionFactory.createIntentionObject(
methodAnnotationMap.get(annotationKey)))) {
result.add(true);
}else{
result.add(false);
}
}
}
}else{
return false;
}
}
if(result.contains(false)){
return false;
}else{
return true;
}
……………………
}
}
|
1)创建一个 Java 项目,添加对 JUnit4 扩展的引用。项目的结构如下:
图 5. JUnit4 扩展示例程序的项目结构
2)创建一个简单的待测试类 Demo 类。
清单 6. 待测试类
public class Demo {
public int add(int a, int b){
return a + b;
}
public int minus(int a, int b){
return a - b;
}
} |
3)创建一个 JUnit4 风格的测试用例 DemoTest 类,对上述 Demo 类的方法编写测试,并将我们自定义的
Annotation 元数据嵌入到 DemoTest 的测试方法中。
清单 7. 包含了自定义 Annotation 的测试用例
public class DemoTest {
@Test
@Feature("Test Add Feature")
@Release("9.9")
public void testAdd() {
Demo d = new Demo();
Assert.assertEquals(4, d.add(1, 2));
}
@Test
@Release("9.9")
public void testMinus() {
Demo d = new Demo();
Assert.assertEquals(2, d.minus(2, 1));
}
} |
4)编写 Main 类来执行测试用例,输入自定义的执行测试用例的条件“Release=9.9,Feature=Test
Add Feature”,来执行 9.9 Release 中跟 Add Feature 相关的测试用例方法,而不执行跟
Minus Feature 相关的测试用例方法。
清单 8. 调用 JUnit4 扩展来执行测试用例
public class Main {
public static void main(String... args){
new JUnitExtensionCore().runMain(args);
}
} |
图 6. 自定义执行测试用例的条件
5) 执行结果:testAdd() 方法满足执行的条件,它执行了。testMinus() 方法不满足执行条件,它没有执行。
图 7. 测试用例执行结果
6)改变自定义的执行条件为“Release=9.9”,执行跟 9.9 Release 相关的所有测试用例方法。
图 8. 自定义执行测试用例的条件
7) 执行结果:testAdd() 方法和 testMinus() 方法都满足执行条件,都执行了。
图 9. 测试用例执行结果
通过上述的代码示例我们可以看出,我们通过对 JUnit4 进行扩展,从而可以自定义测试用例执行的条件,将测试用例的执行和具体的业务功能结合在一起,快速地根据业务功能来执行相应的测试用例。这种细粒度的,以业务属性来组织测试用例的方法,更加适合以测试用例为本的测试驱动开发的需求。可以实现快速地运行目标测试用例,从而促进测试驱动开发在项目中更好地实践。
学习
- “JUnit
4 抢先看”(developerWorks,2005 年 10 月):详细介绍了如何在自己的工作中使用
JUnit 4 框架。本文假设读者具有 JUnit 的使用经验。
- “深入探索
JUnit 4”(developerWorks,2007 年 3 月):介绍了如何充分利用
JUnit 4 由注释实现的新功能,包括参数测试、异常测试及计时测试。
- “单元测试利器
JUnit 4”(developerWorks,2007 年 2 月):本文主要介绍了如何使用
JUnit 4 提供的各种功能开展有效的单元测试,并通过一个实例演示了如何使用 Ant 执行自动化的单元测试。
- “探索
JUnit 4.4 新特性”(developerWorks,2008 年 9 月):本文通过理论分析和详细例子向读者阐述
JUnit 4.4 所带来的最新特性。
- “扩展
JUnit 测试并行程序”(developerWorks,2008 年 12 月):本文将介绍一种对
JUnit 框架的扩展,从而使得并行程序的测试变得如同串行程序一样简单。
-
技术书店:浏览关于这些和其他技术主题的图书。
-
developerWorks Java 技术专区:数百篇关于 Java 编程各个方面的文章。
讨论
|