深入JUnit源码之Builder、Request与JUnitCore

简介:
初次用文字的方式记录读源码的过程,不知道怎么写,感觉有点贴代码的嫌疑。不过中间还是加入了一些自己的理解和心得,希望以后能够慢慢的改进,感兴趣的童鞋凑合着看吧,感觉JUnit这个框架还是值得看的,里面有许多不错的设计思想在,更何况它是Kent Beck和Erich Gamma这样的大师写的。。。。。

深入JUnit源码之BuilderRequestJUnitCore

经过前面三节的RunnerStatementRule的讲解,事实上JUnit的核心运行逻辑已经完成了,剩下的就是一些外围的支持代码,包括Runner的构建、用Assert对测试方法的运行结果的验证代码以及为了兼容性而存在的一些代码。本节将关注Runner的构建部分,在JUnit中通过RequestRunnerBuilder共同支持。

JUnit中,RunnerBuilder对根据测试类创建Runner逻辑的封装,特别是它支持@RunWith注解以在测试类中指定需要的执行的Runner,这也是自定义的Runner可以很方便的插入JUnit框架运行的原因,这个设计其实也蛮具有参考价值的:通过注解的方式为用户提供插入点,以扩展框架本身的功能;而在实现工程中,通过外围Builder来支持,从而不影响核心设计。

Request有点类似配置实例的感觉,用户可以根据自己的需求来定制Runner创建的信息,如代理给RunnerBuilder为每个测试类创建相应的Runner,根据用户的需求以决定是否要有filtersort操作等。用户可以定义自己的Request实例,并传递给JUnitCore以实现自己特定的需求。

在现实中有可能存在这样的一个需求,即用户想同时运行多个测试类,而且这些测试类之间又是相互独立的,在JUnit中,使用Suite来表达这个需求,但是在Request中并没有一个单独的Request接收多个Class实例以创建Suite这个Runner,而是它使用了一个独立的类Computer来完成这样的功能,在Request中定义一个静态方法来处理这个问题。不过我很奇怪为什么要这么做,这个设计和现存的编程模型是格格不入的,个人不太赞同。

大体介绍了RunnerBuilderRequest的用途和设计,接下来将详细介绍他们的源码实现,先从RunnerBuilder开始。

RunnerBuilder

RunnerBuilder的核心就是根据给出的Class实例,创建相应的Runner。一般创建Runner遵循的逻辑是:

1.       如果Class中有@Ignored注解,那么将创建IgnoredClassRunner,该Runner在运行时什么都不做,只是触发testIgnored事件。

2.       如果Class中有@RunWith注解,则使用@RunWith注解中指定的Runner

3.       如果Class中有静态suite()方法的存在,则使用SuiteMethod这个Runner(兼容JUnit3)。

4.       如果Class继承了TestCase类,则使用JUnit38ClassRunner(兼容JUnit3)。

5.       否则,使用BlockJUnit4ClassRunnerJUnit4中默认的Runner)。

其实这个逻辑就是AllDefaultPossibilitiesBuilder中构造Runner的实现。在JUnit内部大量的使用这个RunnerBuilder来构造Runner。其源码如下:

 1 public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
 2     private final boolean fCanUseSuiteMethod;
 3     public AllDefaultPossibilitiesBuilder(boolean canUseSuiteMethod) {
 4        fCanUseSuiteMethod= canUseSuiteMethod;
 5     }
 6     @Override
 7     public Runner runnerForClass(Class<?> testClass) throws Throwable {
 8        List<RunnerBuilder> builders= Arrays.asList(
 9               ignoredBuilder(),
10               annotatedBuilder(),
11               suiteMethodBuilder(),
12               junit3Builder(),
13               junit4Builder());
14        for (RunnerBuilder each : builders) {
15            Runner runner= each.safeRunnerForClass(testClass);
16            if (runner != null)
17               return runner;
18        }
19        return null;
20     }
21     protected JUnit4Builder junit4Builder() {
22        return new JUnit4Builder();
23     }
24     protected JUnit3Builder junit3Builder() {
25        return new JUnit3Builder();
26     }
27     protected AnnotatedBuilder annotatedBuilder() {
28        return new AnnotatedBuilder(this);
29     }
30     protected IgnoredBuilder ignoredBuilder() {
31        return new IgnoredBuilder();
32     }
33     protected RunnerBuilder suiteMethodBuilder() {
34        if (fCanUseSuiteMethod)
35            return new SuiteMethodBuilder();
36        return new NullBuilder();
37     }
38 }

其中fCanUseSuiteMethod用于表达测试类中静态的suite()方法是否被视为用于获得多个实例运行的方法,这个是为了兼容JUnit3而存在,而且在JUnit内部的使用时一般都是给true。再加上junit3Builder()放在junit4Builder()之前构造RunnerBuilder,表明为了兼容JUnit3JUnit4JUnit3中的风格为首选风格。

这里将不对为了兼容JUnit3而创建的RunnerBuilder做介绍,因而下面只会介绍IgnoredBuilderAnnotatedBuilderJUnit4Builder,事实上它都太简单了,以至于基本上不用什么介绍了。IgnoredBuilder会检查传入的Class实例是否有@Ignored注解,若有,则创建IgnoredClassRunner,否则返回nullAnnotatedBuilder检查传入的Class实例是否有@RunWith注解,若有,则使用@RunWith注解中指定的Runner,否则,返回null,这里需要注意的是在用户自定义的Runner中,必须包含一个以Class实例作为参数的构造函数,或者以Class实例和RunnerBuilder实例作为参数的构造函数,否则在构造自定义的Runner时会出错;JUnit4Builder直接根据传入的测试类Class的实例创建BlockJUnit4ClassRunner

 1 public class IgnoredBuilder extends RunnerBuilder {
 2     @Override
 3     public Runner runnerForClass(Class<?> testClass) {
 4        if (testClass.getAnnotation(Ignore.class!= null)
 5            return new IgnoredClassRunner(testClass);
 6        return null;
 7     }
 8 }
 9 public class AnnotatedBuilder extends RunnerBuilder {
10     private static final String CONSTRUCTOR_ERROR_FORMAT= "Custom runner class %s should have a public constructor with signature %s(Class testClass)";
11     private RunnerBuilder fSuiteBuilder;
12     public AnnotatedBuilder(RunnerBuilder suiteBuilder) {
13        fSuiteBuilder= suiteBuilder;
14     }
15     @Override
16     public Runner runnerForClass(Class<?> testClass) throws Exception {
17        RunWith annotation= testClass.getAnnotation(RunWith.class);
18        if (annotation != null)
19            return buildRunner(annotation.value(), testClass);
20        return null;
21     }
22     public Runner buildRunner(Class<? extends Runner> runnerClass,
23            Class<?> testClass) throws Exception {
24        try {
25            return runnerClass.getConstructor(Class.class).newInstance(
26                   new Object[] { testClass });
27        } catch (NoSuchMethodException e) {
28            try {
29               return runnerClass.getConstructor(Class.class,
30                      RunnerBuilder.class).newInstance(
31                      new Object[] { testClass, fSuiteBuilder });
32            } catch (NoSuchMethodException e2) {
33               String simpleName= runnerClass.getSimpleName();
34               throw new InitializationError(String.format(
35                      CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName));
36            }
37        }
38     }
39 }
40 public class JUnit4Builder extends RunnerBuilder {
41     @Override
42     public Runner runnerForClass(Class<?> testClass) throws Throwable {
43        return new BlockJUnit4ClassRunner(testClass);
44     }
45 }

RunBuilder类本身也定义了一些方法,以帮助其他Runner,如Suite,构建其内部通过其他方式取到的测试类Class实例。这里对parent字段的存在有必要解释一下,因为我刚开始看到的时候也很费解,addParent()方法只在一个方法中调用一次,而且就这个类来看也不存在递归,为什么会有对相同parents的验证?要解释这个问题,需要知道Suite的构造函数还会调用runners()方法,加入有一次调用parent为一个使用Suite的类,这个类同时又在children中出现,那么在调用该方法使将给类加入到parents中,而后在构造children中的该类时又会调用该方法,将想用的Class实例加入parents中,从而引起异常。

 1 public abstract class RunnerBuilder {
 2     private final Set<Class<?>> parents= new HashSet<Class<?>>();
 3     public abstract Runner runnerForClass(Class<?> testClass) throws Throwable;
 4     public Runner safeRunnerForClass(Class<?> testClass) {
 5        try {
 6            return runnerForClass(testClass);
 7        } catch (Throwable e) {
 8            return new ErrorReportingRunner(testClass, e);
 9        }
10     }
11     Class<?> addParent(Class<?> parent) throws InitializationError {
12        if (!parents.add(parent))
13            throw new InitializationError(String.format("class '%s' (possibly indirectly) contains itself as a SuiteClass", parent.getName()));
14        return parent;
15     }
16     void removeParent(Class<?> klass) {
17        parents.remove(klass);
18     }
19     public List<Runner> runners(Class<?> parent, Class<?>[] children)
20            throws InitializationError {
21        addParent(parent);
22        try {
23            return runners(children);
24        } finally {
25            removeParent(parent);
26        }
27     }
28     public List<Runner> runners(Class<?> parent, List<Class<?>> children)
29            throws InitializationError {
30        return runners(parent, children.toArray(new Class<?>[0]));
31     }
32     private List<Runner> runners(Class<?>[] children) {
33        ArrayList<Runner> runners= new ArrayList<Runner>();
34        for (Class<?> each : children) {
35            Runner childRunner= safeRunnerForClass(each);
36            if (childRunner != null)
37               runners.add(childRunner);
38        }
39        return runners;
40     }
41 }

最后来看一下RunnerBuilder的类结构图吧,了解一下目前存在的几个RunnerBuilder


Request

Request是对RunnerBuilder的封装,它提供了改变RunnerBuilder创建出的Runner的接口,如创建Runner后,用FilterSorter过滤或重新排列测试方法的顺序。就目前JUnit只有FilterSorter可以对Runner做一些自定义的配置。Filter可以定义那些测试方法是可以运行的,比如在eclipse中提供的对一个测试方法单独运行就是使用它来实现;或者用户可以自己定义一个可以运行方法的集合,然后只要遇到这样的方法,然后根据这个集合来编写自定义的FilterSorter则用于排列Runner内部测试方法的执行顺序,但是这个定制只是对一个Runner中的测试方法有用,它并不会排列跨Runner之间的测试方法。不废话了,先来看一下Request的类结构图吧。


Request的结构比较简单,而且代码实现也比较简单,Request是一个抽象类,它定义了一个getRunner()的抽象方法,这个方法只是返回一个Runner实例。其中ClassRequest根据一个测试类,使用AllDefaultPossibilitiesBuilder创建一个RunnerFilterRequest则以一个RequestFilter实例为构造参数,在实现getRunner()方法时,根据传入的Request获取Runner,并对改Runner应用传入的Filter以过滤掉那些不需要运行的测试方法;SortingRequest也是以一个RequestComparator<Description>为构造参数,在实现getRunner()方法是,根据传入的Request获取Runner,并根据comparator构造Sorter对刚获取到的Runner排序。这些实现有点Decorator模式的味道。

 1 public class ClassRequest extends Request {
 2     private final Class<?> fTestClass;
 3     private boolean fCanUseSuiteMethod;
 4     public ClassRequest(Class<?> testClass, boolean canUseSuiteMethod) {
 5        fTestClass= testClass;
 6        fCanUseSuiteMethod= canUseSuiteMethod;
 7     }
 8     public ClassRequest(Class<?> testClass) {
 9        this(testClass, true);
10     }
11     @Override
12     public Runner getRunner() {
13        return new AllDefaultPossibilitiesBuilder(fCanUseSuiteMethod).safeRunnerForClass(fTestClass);
15     }
16 }
17 public final class FilterRequest extends Request {
18     private final Request fRequest;
19     private final Filter fFilter;
20     public FilterRequest(Request classRequest, Filter filter) {
21        fRequest= classRequest;
22        fFilter= filter;
23     }
24     @Override
25     public Runner getRunner() {
26        try {
27            Runner runner= fRequest.getRunner();
28            fFilter.apply(runner);
29            return runner;
30        } catch (NoTestsRemainException e) {
31            return new ErrorReportingRunner(Filter.classnew Exception(String
32                   .format("No tests found matching %s from %s", fFilter
33                          .describe(), fRequest.toString())));
34        }
35     }
36 }
37 public class SortingRequest extends Request {
38     private final Request fRequest;
39     private final Comparator<Description> fComparator;
40     public SortingRequest(Request request, Comparator<Description> comparator) {
41        fRequest= request;
42        fComparator= comparator;
43     }
44     @Override
45     public Runner getRunner() {
46        Runner runner= fRequest.getRunner();
47        new Sorter(fComparator).apply(runner);
48        return runner;
49     }
50 }
51 public abstract class Request {
52     public abstract Runner getRunner();
53 }

除了Request类结构,Request类本身还提供了多个工场方法,以一种不需要知道Request类结构的方法创建Request,也算是一种封装吧,使用起来比较方便,而且随着框架的演化,可以添加或删除子类而不需要考虑用户是否使用了某个子类。如果做的安全一些、然后不考虑测试的话,可以把FilterRequestSortingRequest的可见性降低,如包级别的。除了一些静态的工场方法,RequestFilterSorter也提供了各自的方法支持,在我们得到一个Request的引用后,只需要调用这两个方法即可构造需要的RequestFilterRequestSortingRequest)。

 1 public abstract class Request {
 2     public static Request method(Class<?> clazz, String methodName) {
 3        Description method= Description.createTestDescription(clazz, methodName);
 4        return Request.aClass(clazz).filterWith(method);
 5     }
 6     public static Request aClass(Class<?> clazz) {
 7        return new ClassRequest(clazz);
 8     }
 9     public static Request classWithoutSuiteMethod(Class<?> clazz) {
10        return new ClassRequest(clazz, false);
11     }
12     public static Request classes(Computer computer, Class<?> classes) {
13        try {
14            AllDefaultPossibilitiesBuilder builder= new AllDefaultPossibilitiesBuilder(true);
15            Runner suite= computer.getSuite(builder, classes);
16            return runner(suite);
17        } catch (InitializationError e) {
18            throw new RuntimeException(
19                   "Bug in saff's brain: Suite constructor, called as above, should always complete");
20        }
21     }
22     public static Request classes(Class<?> classes) {
23        return classes(JUnitCore.defaultComputer(), classes);
24     }
25     public static Request runner(final Runner runner) {
26        return new Request(){
27            @Override
28            public Runner getRunner() {
29               return runner;
30            }     
31        };
32     }
33     public Request filterWith(Filter filter) {
34        return new FilterRequest(this, filter);
35     }
36     public Request filterWith(final Description desiredDescription) {
37        return filterWith(Filter.matchMethodDescription(desiredDescription));
38     }
39     public Request sortWith(Comparator<Description> comparator) {
40        return new SortingRequest(this, comparator);
41     }
42 }
43 public class Computer {
44     public static Computer serial() {
45        return new Computer();
46     }
47     public Runner getSuite(final RunnerBuilder builder,
48            Class<?>[] classes) throws InitializationError {
49        return new Suite(new RunnerBuilder() {
50            @Override
51            public Runner runnerForClass(Class<?> testClass) throws Throwable {
52               return getRunner(builder, testClass);
53            }
54        }, classes);
55     }
56     protected Runner getRunner(RunnerBuilder builder, Class<?> testClass) throws Throwable {
57        return builder.runnerForClass(testClass);
58     }
59 }

 

这里对Computer这个类引入的意义一直没有弄明白,为什么不直接在Request.classes()方法中创建Suite?即使要提取到Computer中以给创建Runner提供扩展点,直接在getSuite()方法中使用builder创建Suite就可以了啊,但是又要将getRunner()方法提取出来,这个提取可以给子类在根据buildertestClass创建Runner提供扩展点,但是以我目前的水平,还是看不多这个扩展点存在的意义。

JUnitCore

JUnitCoreJUnit中运行Request的门面类,同时它也提供了对命令模式的测试实现,它接收多个测试类作为参数,然后运行这些测试类中的所有测试方法。其实现是从传入的参数中取到所有的测试类的Class实例,然后根据这些测试类的Class实例创建Request实例,从创建的Request实例中可以取得Runner实例,运行该Runner,并处理事件逻辑,最后如果所有测试通过,则退出值为0,否则为1。为了统计测试结果信息,JUnit还提供了一个默认的RunListener实现:TextRunListener,这个Listener在每个测试方法开始的时候打印一个点’.’,当一个测试方法失败是打印E,当一个测试方法被忽略时打印I,当所有测试方法执行完成后打印总体统计时间,如运行时间、所有错误信息的异常堆栈以及最后成功多少、失败多少等信息。对于基于JUnit编写更适合项目本身的测试运行的用户来说,最重要的就是几个run()方法,这些用户可以通过实现自己特定的逻辑以创建出符合自己需求的Request或通过某种方式查找到所有自己要运行的测试类等,然后调用你需要的run()方法。

 1 public class JUnitCore {
 2     private RunNotifier fNotifier;
 3     public JUnitCore() {
 4        fNotifier= new RunNotifier();
 5     }
 6     public static Result runClasses(Computer computer, Class<?> classes) {
 7        return new JUnitCore().run(computer, classes);
 8     }
 9     public static Result runClasses(Class<?> classes) {
10        return new JUnitCore().run(defaultComputer(), classes);
11     }
12     public Result run(Class<?> classes) {
13        return run(Request.classes(defaultComputer(), classes));
14     }
15     public Result run(Computer computer, Class<?> classes) {
16        return run(Request.classes(computer, classes));
17     }
18     public Result run(Request request) {
19        return run(request.getRunner());
20     }
21     public Result run(Runner runner) {
22        Result result= new Result();
23        RunListener listener= result.createListener();
24        fNotifier.addFirstListener(listener);
25        try {
26            fNotifier.fireTestRunStarted(runner.getDescription());
27            runner.run(fNotifier);
28            fNotifier.fireTestRunFinished(result);
29        } finally {
30            removeListener(listener);
31        }
32        return result;
33     }
34 }

 


相关文章
|
Java 测试技术 Android开发
|
测试技术
关于Junit源码的一些探索
关于Junit源码的一些探讨
1354 0
|
设计模式 自然语言处理
|
设计模式 前端开发
|
3月前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
28天前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
23 5
|
2月前
|
SQL JavaScript 前端开发
基于Java访问Hive的JUnit5测试代码实现
根据《用Java、Python来开发Hive应用》一文,建立了使用Java、来开发Hive应用的方法,产生的代码如下
73 6
|
3月前
|
测试技术
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
48 2
|
3月前
|
测试技术
如何使用 JUnit 测试方法是否存在异常
【8月更文挑战第22天】
58 0