天天看点

深入JUnit源码之Statement深入JUnit源码之Statement

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

看junit源码最大的收获就是看到这个statement的设计,它也是我看到过的所有源码中最喜欢的设计之一。junit中runner的运行过程就是statement链的运行过程,statement是对一个单元运行的封装,每个statement都只是执行它本身所表达的逻辑,而将其他逻辑交给下一个statement处理,而且基本上的statement都存在对下一个节点的引用,从而由此构成一条statement的链,从设计模式的角度上来看,这是一个职责连模式(chain of responsibility pattern)。junit中对@beforeclass、@afterclass、@before、@after、@classrule、@rule等逻辑就是通过statement来实现的。首先来看一下statement的类结构。

深入JUnit源码之Statement深入JUnit源码之Statement

statement的类结构还是比较简单的,首先statement是所有类的父类,它只定义了一个抽象的evaluate()方法,由其他子类实现该方法;而且除了fail和invokemethod类,其他类都有一个对statement本身的引用。其实从实现上,每个statement也是比较简单的,这个接下来就可以看到了。每个statement都只实现它自己的逻辑,而将其他逻辑代理给另一个statement执行,这样可以在编写每个statement的时候只关注自己的逻辑,从而保持statement本身的简单,并且易于扩展,当一条statement执行完后,整个逻辑也就执行完了。不过statement这条链也不是凭空产生的,它也是要根据一定的逻辑构造起来的,关于statement链的构造在junit中由runner负责,为了保持本文的完整性,本文会首先会讲解上述几个statement的源码,同时简单回顾statement链的构造过程,最后将通过一个简单的例子,将statement的执行过程用序列图的方式表达出来,以更加清晰的表达statement的执行过程。不过本文不会详细介绍rules相关的代码,这部分的代码将会在下一节详细介绍。

这两个statement是针对junit中@beforeclass、@before的实现的,其中@beforeclass是在测试类运行时,所有测试方法运行之前运行,并且对每个测试类只运行一次,这个注解修饰的方法必须是静态的(在runner一节中有谈过它为什么要被设计成一定是要静态方法,因为在运行每个测试方法是,测试类都会从新初始化一遍,如果不是静态类,它只运行一次的话,它运行的结果无法保存下来);@before是在每个测试方法运行之前都要运行。

在statement的设计中,@beforeclass注解的方法抽象成一个statement叫runbefores,而测试类中其他要运行的测试方法的运行过程是另一个statement叫next,在runbefores中调用完所有这些方法,而将其他逻辑交给next; @before注解的方法也是一样的逻辑,它把接下来的测试方法看成是一个statement叫next,它调用完所有@before注解的方法后,将接下来的事情交给next,因而他们共享runbefores的statement,唯一不同的是@beforeclass的runbefores可以直接调用测试类中的方法,因为他们是静态的,而@before的runbefores需要传入测试类的实例。

 1 public class runbefores extends statement {

 2     private final statement fnext;

 3     private final object ftarget;

 4     private final list<frameworkmethod> fbefores;

 5     public runbefores(statement next, list<frameworkmethod> befores, object target) {

 6        fnext= next;

 7        fbefores= befores;

 8        ftarget= target;

 9     }

10     @override

11     public void evaluate() throws throwable {

12        for (frameworkmethod before : fbefores)

13            before.invokeexplosively(ftarget);

14        fnext.evaluate();

15     }

16 }

从源码中可以看到,构造runbefores时传入下一个statement、所有@beforeclass或@before注解的方法以及测试类的实例,对@beforeclass来说,传入null即可。在运行evaluate()方法时,它依次调用@beforeclass或@before注解的方法,后将接下来的逻辑交给next。从这段逻辑也可以看出如果有一个@beforeclass或@before注解的方法抛异常,接下来的这些方法就都不会执行了,包括测试方法。不过此时@afterclass或@after注解的方法还会执行,这个在下一小节中即可知道。

关于runbefores的构造要,其实最重要的是要关注它的next statement是什么,对于@beforeclass对应的runbefores来说,它的next statement那些所有的测试方法运行而组成的statement,而对于@before对应的runbefores来说,它的next statement是测试方法的statement:

 1 protected statement classblock(final runnotifier notifier) {

 2     statement statement= childreninvoker(notifier);

 3     statement= withbeforeclasses(statement);

 4     

深入JUnit源码之Statement深入JUnit源码之Statement

 5 }

 6 protected statement withbeforeclasses(statement statement) {

 7     list<frameworkmethod> befores= ftestclass

 8            .getannotatedmethods(beforeclass.class);

 9     return befores.isempty() ? statement :

10        new runbefores(statement, befores, null);

11 }

12 protected statement methodblock(frameworkmethod method) {

13     

深入JUnit源码之Statement深入JUnit源码之Statement

14     statement statement= methodinvoker(method, test);

15     

深入JUnit源码之Statement深入JUnit源码之Statement

16     statement= withbefores(method, test, statement);

17     

深入JUnit源码之Statement深入JUnit源码之Statement

18 }

19 protected statement withbefores(frameworkmethod method, object target,

20        statement statement) {

21     list<frameworkmethod> befores= gettestclass().getannotatedmethods(

22            before.class);

23     return befores.isempty() ? statement : new runbefores(statement,

24            befores, target);

25 }

这两个statement是针对junit中@afterclass、@after的实现的,其中@afterclass是在测试类运行时,所有测试方法结束之后运行,不管之前的方法是否有抛异常,并且对每个测试类只运行一次,这个注解修饰的方法必须是静态的;@after是在每个测试方法运行结束后都要运行的,不管测试方法是否测试失败。

在statement的设计中,@afterclass注解的方法抽象成一个statement叫afters,而测试类中之前要运行的过程是另一个statement叫next(其实这个叫before更好一些),在runafters中等所有之前的运行过程调用完后,再调用@afterclass注解的方法; @after注解的方法也是一样的逻辑,它把之前的测试方法包括@before注解的方法看成是一个statement叫next(before?),它等测试方法或@before注解的方法调用完后,调用@after注解的方法,因而他们共享runafters的statement,唯一不同的是@afterclass的runafters可以直接调用测试类中的方法,因为他们是静态的,而@after的runafters需要传入测试类的实例。

 1 public class runafters extends statement {

 4     private final list<frameworkmethod> fafters;

 5     public runafters(statement next, list<frameworkmethod> afters, object target) {

 7        fafters= afters;

12        list<throwable> errors = new arraylist<throwable>();

13        try {

14            fnext.evaluate();

15        } catch (throwable e) {

16            errors.add(e);

17        } finally {

18            for (frameworkmethod each : fafters)

19               try {

20                   each.invokeexplosively(ftarget);

21               } catch (throwable e) {

22                   errors.add(e);

23               }

24        }

25        multiplefailureexception.assertempty(errors);

26     }

27 }

从源码中可以看到,构造runafters时传入下一个statement、所有@afterclass或@after注解的方法以及测试类的实例,对@afterclass来说,传入null即可。在运行evaluate()方法时,它会等之前的statement执行结束后,再依次调用@afterclass或@after注解的方法。从这段逻辑也可以看出无论之前statement执行是否抛异常,@afterclass或@after注解的方法都是会被执行的,为了避免在执行@afterclass或@after注解的方法抛出的异常覆盖之前在运行@beforeclass、@before或@test注解方法抛出的异常,这里所有的异常都会触发一次testfailure的事件,这个实现可以查看runner小节的eachtestnotifier类的实现。

对于runafters的构造,可能要注意的一点是传入runafters的statement是runbefores的实例,这个其实还是好理解的,因为runafters是在传入的statement运行结束后运行,而runbefores又是要在测试方法之前运行的,因而需要将runafters放在statement链的最头端,而后是runafters,最后才是测试方法调用的statement(invokemethod)。

 2     

深入JUnit源码之Statement深入JUnit源码之Statement

 4     statement= withbeforeclasses(statement);

 5     

深入JUnit源码之Statement深入JUnit源码之Statement

 6 }

 7 protected statement withafterclasses(statement statement) {

 8     list<frameworkmethod> afters= ftestclass

 9            .getannotatedmethods(afterclass.class);

10     return afters.isempty() ? statement :

11        new runafters(statement, afters, null);

12 }

13 protected statement methodblock(frameworkmethod method) {

14     

深入JUnit源码之Statement深入JUnit源码之Statement

15     statement= withbefores(method, test, statement);

16     statement= withafters(method, test, statement);

17     

深入JUnit源码之Statement深入JUnit源码之Statement

19 protected statement withafters(frameworkmethod method, object target,

21     list<frameworkmethod> afters= gettestclass().getannotatedmethods(

22            after.class);

23     return afters.isempty() ? statement : new runafters(statement, afters,

24            target);

25 }  

之所有要把这三个statement放在一起是因为他们都是和@test注解相关的:

1 @retention(retentionpolicy.runtime)

2 @target({elementtype.method})

3 public @interface test {

4     class<? extends throwable> expected() default none.class;

5     long timeout() default 0l;

6 }

@test注解定义了两个成员:expected指定当前测试方法如果抛出指定的异常则表明测试成功;而timeout指定当前测试方法如果超出指定的时间(以毫秒为单位),则测试失败。在statement设计中,这些逻辑都抽象成了一个statement。而@test注解的方法则被认为是真正要运行的测试方法,它的执行过程也被抽象成了一个statement。

@test注解的方法抽象出的statement命名为invokemethod,它是一个非常简单的statement:

 1 public class invokemethod extends statement {

 2     private final frameworkmethod ftestmethod;

 3     private object ftarget;

 4     public invokemethod(frameworkmethod testmethod, object target) {

 5        ftestmethod= testmethod;

 6        ftarget= target;

 7     }

 8     @override

 9     public void evaluate() throws throwable {

10        ftestmethod.invokeexplosively(ftarget);

11     }

使用一个方法实例和测试类的实例构造invokemethod,在运行时直接调用该方法。并且invokemethod并没有对其他statement的引用,因而它是statement链上的最后一个节点。

 1 protected statement methodblock(frameworkmethod method) {

深入JUnit源码之Statement深入JUnit源码之Statement

 3     statement statement= methodinvoker(method, test);

 4     statement= possiblyexpectingexceptions(method, test, statement);

 5     statement= withpotentialtimeout(method, test, statement);

 6     

深入JUnit源码之Statement深入JUnit源码之Statement

 7 }

 8 protected statement methodinvoker(frameworkmethod method, object test) {

 9     return new invokemethod(method, test);

10 }

expectexception用于处理当在@test注解中定义了expected字段时,该注解所在的方法是否在运行过程中真的抛出了指定的异常,如果没有,则表明测试失败,因而它需要该测试方法对应的statement(invokemethod)的引用:

 1 public class expectexception extends statement {

 2     private statement fnext;

 3     private final class<? extends throwable> fexpected;

 4     public expectexception(statement next, class<? extends throwable> expected) {

 5        fnext= next;

 6        fexpected= expected;

 9     public void evaluate() throws exception {

10        boolean complete = false;

11        try {

12            fnext.evaluate();

13            complete = true;

14        } catch (assumptionviolatedexception e) {

15            throw e;

16        } catch (throwable e) {

17            if (!fexpected.isassignablefrom(e.getclass())) {

18               string message= "unexpected exception, expected<"

19                          + fexpected.getname() + "> but was<"

20                          + e.getclass().getname() + ">";

21               throw new exception(message, e);

22            }

23        }

24        if (complete)

25            throw new assertionerror("expected exception: "

26                   + fexpected.getname());

27     }

28 }

使用invokemethod实例和一个expected的throwable class实例作为参数构造expectexception,当invokemethod实例执行后,判断其抛出的异常是否为指定的异常或者该测试方法没有抛出异常,在这两种情况下,测试都会失败,因而需要它抛出异常以处理这种情况。

深入JUnit源码之Statement深入JUnit源码之Statement
深入JUnit源码之Statement深入JUnit源码之Statement

 7 protected statement possiblyexpectingexceptions(frameworkmethod method,

 8        object test, statement next) {

 9     test annotation= method.getannotation(test.class);

10     return expectsexception(annotation) ? new expectexception(next,

11            getexpectedexception(annotation)) : next;

failontimeout是在@test注解中指定了timeout值时,用于控制@test注解所在方法的执行时间是否超出了timeout的值,若是,则抛出异常,表明测试失败。在junit4当前的实现中,它引用的statement实例是expectexception(如果expected字段定义了的话)或invokemethod。它通过将引用的statement实例的执行放到另一个线程中,然后通过控制线程的执行时间以控制内部引用的statement实例的执行时间,如果测试方法因内部抛出异常而没有完成,则抛出内部抛出的异常实例,否则抛出执行时间超时相关的异常。

 1 public class failontimeout extends statement {

 2     private final statement foriginalstatement;

 3     private final long ftimeout;

 4     public failontimeout(statement originalstatement, long timeout) {

 5        foriginalstatement= originalstatement;

 6        ftimeout= timeout;

10        statementthread thread= evaluatestatement();

11        if (!thread.ffinished)

12            throwexceptionforunfinishedthread(thread);

13     }

14     private statementthread evaluatestatement() throws interruptedexception {

15        statementthread thread= new statementthread(foriginalstatement);

16        thread.start();

17        thread.join(ftimeout);

18        thread.interrupt();

19        return thread;

20     }

21     private void throwexceptionforunfinishedthread(statementthread thread)

22            throws throwable {

23        if (thread.fexceptionthrownbyoriginalstatement != null)

24            throw thread.fexceptionthrownbyoriginalstatement;

25        else

26            throwtimeoutexception(thread);

28     private void throwtimeoutexception(statementthread thread) throws exception {

29        exception exception= new exception(string.format(

30               "test timed out after %d milliseconds", ftimeout));

31        exception.setstacktrace(thread.getstacktrace());

32        throw exception;

33     }

34     private static class statementthread extends thread {

35        private final statement fstatement;

36        private boolean ffinished= false;

37        private throwable fexceptionthrownbyoriginalstatement= null;

38        public statementthread(statement statement) {

39            fstatement= statement;

40        }

41        @override

42        public void run() {

43            try {

44               fstatement.evaluate();

45               ffinished= true;

46            } catch (interruptedexception e) {

47               //don't log the interruptedexception

48            } catch (throwable e) {

49               fexceptionthrownbyoriginalstatement= e;

50            }

51        }

52     }

53 }

failontimeout的构造过程如同上述的其他statement类似:

深入JUnit源码之Statement深入JUnit源码之Statement
深入JUnit源码之Statement深入JUnit源码之Statement

 8 protected statement withpotentialtimeout(frameworkmethod method,

 9        object test, statement next) {

10     long timeout= gettimeout(method.getannotation(test.class));

11     return timeout > 0 ? new failontimeout(next, timeout) : next;

fail这个statement是在创建测试类出错时构造的statement,这个设计思想有点类似null object模式,即保持编程模型的统一,即使在出错的时候也用一个statement去封装,这也正是runner中errorreportingrunner的设计思想。

fail这个statement很简单,它在运行时重新抛出之前记录的异常,其构造过程也是在创建测试类实例出错时构造:

 2     object test;

 3     try {

 4        test= new reflectivecallable() {

 5            @override

 6            protected object runreflectivecall() throws throwable {

 7               return createtest();

 8            }

 9        }.run();

10     } catch (throwable e) {

11        return new fail(e);

12     }

深入JUnit源码之Statement深入JUnit源码之Statement

14 }

15 public class fail extends statement {

16     private final throwable ferror;

17     public fail(throwable e) {

18        ferror= e;

19     }

20     @override

21     public void evaluate() throws throwable {

22        throw ferror;

23     }

24 }

runrules这个statement是对@classrule和@rule运行的封装,它会将定义的所有rule应用到传入的statement引用后返回,由于它内部还有一些比较复杂的逻辑,关于rule将有一个单独的小节讲解。这里主要关注runrules这个statement的实现和构造:

 1 public class runrules extends statement {

 2     private final statement statement;

 3     public runrules(statement base, iterable<testrule> rules, description description) {

 4        statement= applyall(base, rules, description);

 5     }

 6     @override

 7     public void evaluate() throws throwable {

 8        statement.evaluate();

10     private static statement applyall(statement result, iterable<testrule> rules,

11            description description) {

12        for (testrule each : rules)

13            result= each.apply(result, description);

14        return result;

17 protected statement classblock(final runnotifier notifier) {

18     

深入JUnit源码之Statement深入JUnit源码之Statement

19     statement= withafterclasses(statement);

20     statement= withclassrules(statement);

21     return statement;

22 }

23 private statement withclassrules(statement statement) {

24     list<testrule> classrules= classrules();

25     return classrules.isempty() ? statement :

26         new runrules(statement, classrules, getdescription());

28 protected statement methodblock(frameworkmethod method) {

29     

深入JUnit源码之Statement深入JUnit源码之Statement

30     statement= withrules(method, test, statement);

31     return statement;

32 }

33 private statement withrules(frameworkmethod method, object target,

34        statement statement) {

35     list<testrule> testrules= gettestclass().getannotatedfieldvalues(

36         target, rule.class, testrule.class);

37     return testrules.isempty() ? statement :

38        new runrules(statement, testrules, describechild(method));

39 }

rule分为@classrule和@rule,@classrule是测试类级别的,它对一个测试类只运行一次,而@rule是测试方法级别的,它在每个测试方法运行时都会运行。runrules的构造过程中,我们可以发现runrules才是最外层的statement,testrule中要执行的逻辑要么比@beforeclass、@before注解的方法要早,要么比@afterclass、@after注解的方法要迟。

假设在一个测试类中存在多个@beforeclass、@afterclass、@before、@after注解的方法,并且有两个测试方法testmethod1()和testmethod2(),那么他们的运行序列图如下所示(这里没有考虑rule,对@classrule的runrules,它的链在最前端,而@rule的runrules则是在runafters的前面,关于rule将会在下一节中详细讲解):

深入JUnit源码之Statement深入JUnit源码之Statement

继续阅读