求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
扩展 JUnit4 以促进测试驱动开发

2010-08-09 作者:左超,赵汇川 来源:IBM

 

内容

简介: 在采用测试驱动开发的项目中,有一个经常困扰开发者的问题是:当存在大量的测试用例时,一次运行完所有的测试用例要花费很长的时间,采用 TestSuite 来组织测试用例的方式缺乏灵活性,通常它的组织结构大体和 Java Package/Class 的组织结构类似,不能和当前实现的业务需求完全相关。本文将通过扩展 JUnit4 来实现一种可以更加高效灵活地组织和运行测试用例的解决方案,促进测试驱动开发实践更有效地进行。

实际 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 核心类之间的调用关系
图 1. JUnit4 核心类之间的调用关系

(查看图 1 的 清晰版本。)

在 JUnit4 中,Runner 类定义了运行测试用例的接口,默认提供的 Runner 实现类有 Suite、BlockJUnit4ClassRunner、Parameterized 等等。Suite 类相当于 JUnit3 中的 TestSuite,BlockJUnit4ClassRunner 用来执行单个的测试用例。BlockJUnit4ClassRunner 关联了一个 TestClass 类,TestClass 封装了测试用例的 Class 元数据,可以访问到测试用例的 method、annotation 等。FrameworkMethod 封装了测试用例方法的元数据。从下图中我们可以看到这些类的关系。

图 2. JUnit4 核心类
图 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. 封装用户输入的执行条件的类

过滤器是用来根据用户输入,对目标测试用例和测试方法进行过滤,从而找到符合条件的测试用例方法。用户输入的每个条件都会生出相应的一个过滤器,只有测试用例满足过滤器链中所有的过滤条件,测试用例才能被执行。下面的清单展示了过滤器接口的定义和过滤器工厂的核心实现。过滤器工厂会根据用户输入的条件来创建对应的过滤器。

清单 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 中类的继承关系
图 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 扩展示例程序的项目结构
图 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. 自定义执行测试用例的条件
图 6. 自定义执行测试用例的条件

5) 执行结果:testAdd() 方法满足执行的条件,它执行了。testMinus() 方法不满足执行条件,它没有执行。

图 7. 测试用例执行结果
图 7. 测试用例执行结果

6)改变自定义的执行条件为“Release=9.9”,执行跟 9.9 Release 相关的所有测试用例方法。

图 8. 自定义执行测试用例的条件
图 8. 自定义执行测试用例的条件

7) 执行结果:testAdd() 方法和 testMinus() 方法都满足执行条件,都执行了。

图 9. 测试用例执行结果
图 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 编程各个方面的文章。

讨论



Java 中的中文编码问题
Java基础知识的三十个经典问答
玩转 Java Web 应用开发
使用Spring更好地处理Struts
用Eclipse开发iPhone Web应用
插件系统框架分析
更多...   


Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术


Struts+Spring+Hibernate/EJB+性能优化
华夏基金 ActiveMQ 原理与管理
某民航公司 Java基础编程到应用开发
某风电公司 Java 应用开发平台与迁移
日照港 J2EE应用开发技术框架与实践
某跨国公司 工作流管理JBPM
东方航空公司 高级J2EE及其前沿技术
更多...