扩展 JUnit4 以促进测试驱动开发

简介: 在采用测试驱动开发的项目中,有一个经常困扰开发者的问题是:当存在大量的测试用例时,一次运行完所有的测试用例要花费很长的时间,采用 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 编程各个方面的文章。

讨论



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值