扩展 JUnit4 以促进测试驱动开发
2010-07-23 00:00:00 来源:WEB开发网实际 Java 开发中单元测试常遇到的问题
在敏捷开发中,为了提高软件开发的效率和质量,测试驱动开发实践已经被广泛使用。在测试驱动开发的项目中,随着项目开发不断地深入,积累的测试用例会越来越多。测试驱动开发的一个最佳实践是随时运行测试用例,保证任何时候测试用例都能成功执行,从而保证项目的代码是可工作的。当测试用例数量很多时,一次运行所有测试用例所消耗的时间可能会很长,导致运行测试用例的成本很高。所以在实际敏捷开发中,如何组织、运行测试用例以促进测试驱动开发成为一个值得探究的问题。
JUnit 是 Java 开发中最常用的单元测试工具。在 JUnit3 用 TestSuite 来显式地组织想要运行的 TestCase,通常 TestSuite 的组织大体上和 Java Package/Class 的组织类似,但这样并不能和当前正在实现的业务需求完全相关,显得比较笨拙,比如说要运行某个子模块下所有的 TestCase,或者运行跟某个具体功能相关的 TestCase,涉及到的 TestCase 数量可能较多,采用定义 TestSuite 的方式一个个地添加 TestCase 很低效并且繁琐。在 JUnit4 中同样只能显式地组织要运行的 TestCase。
怎么样解决这些问题,新发布的 JUnit4 提供了开发人员扩展的机制,可以通过对 JUnit 进行扩展来提供一种解决的方法。
JUnit4 的新特性和扩展机制
JUnit4 引入了 Java5 的 Annotation 机制,来简化原有的使用方法。测试用例不再需要继承 TestCase 类,TestSuite 类也取消了,改用 @Suite.SuiteClasses 来组织 TestCase。但是这种还是通过显示指定 TestCase 来组织运行的结构,不能解决上述的问题。关于 JUnit4 的新特性具体可以参考 developerworks 的文章。
JUnit4 的实现代码中提供了 Runner 类来封装测试用例的执行。它本身提供了 Runner 的多种实现,比如 ParentRunner 类、Suite 类,BlockJUnit4ClassRunner 类。我们可以充分利用 JUnit4 提供的已有设施来对它进行扩展,实现我们期望的功能。
首先我们来分析一下 JUnit4 在运行一个测试用例时,它内部的核心类是如何工作的。图 1 展示了 JUnit4 运行测试用例时,核心类之间的调用关系。
图 1. JUnit4 核心类之间的调用关系
查看原图(大图)
在 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 扩展的实现
下面我们来描述一下对 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;
}
……………………
}
}
通过测试用例实例展示 JUnit 扩展的执行效果
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 进行扩展,从而可以自定义测试用例执行的条件,将测试用例的执行和具体的业务功能结合在一起,快速地根据业务功能来执行相应的测试用例。这种细粒度的,以业务属性来组织测试用例的方法,更加适合以测试用例为本的测试驱动开发的需求。可以实现快速地运行目标测试用例,从而促进测试驱动开发在项目中更好地实践。
- ››扩展Axis2框架,支持基于JVM的脚本语言
- ››扩展WebSphere Portal V6个性化功能
- ››扩展JavaScript的时候,千万要保留其原来的所有功...
- ››扩展数据:如何为 Model 750 服务器选择 I/O 扩展...
- ››扩展 JDT 实现自动代码注释与格式化
- ››扩展 secldap 的功能以验证多个数据源
- ››扩展 JUnit4 以促进测试驱动开发
- ››扩展 JUnit 测试并行程序
- ››扩展的ToolStripEx控件
- ››扩展 Eclipse 的 Java 开发工具
- ››扩展 Eclipse 辅助和规范开发流程
- ››扩展方法 DataTable 和List 相互转换
更多精彩
赞助商链接