原文出处:http://www.blogjava.net/DLevin/archive/2012/05/11/377954.html。感谢作者的无私分享。

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

深入JUnit源码之Statement

看JUnit源码最大的收获就是看到这个Statement的设计,它也是我看到过的所有源码中最喜欢的设计之一。JUnit中Runner的运行过程就是Statement链的运行过程,Statement是对一个单元运行的封装,每个Statement都只是执行它本身所表达的逻辑,而将其他逻辑交给下一个Statement处理,而且基本上的Statement都存在对下一个节点的引用,从而由此构成一条Statement的链,从设计模式的角度上来看,这是一个职责连模式(Chain
Of Responsibility Pattern)。JUnit中对@BeforeClass、@AfterClass、@Before、@After、@ClassRule、@Rule等逻辑就是通过Statement来实现的。首先来看一下Statement的类结构。

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

RunBefores和RunAfters

这两个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     }

     @Override

     public void evaluate() throws Throwable {

        for (FrameworkMethod before : fBefores)

            before.invokeExplosively(fTarget);

        fNext.evaluate();

     }

 }

从源码中可以看到,构造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     

 5 }

 6 protected Statement withBeforeClasses(Statement statement) {

 7     List<FrameworkMethod> befores= fTestClass

 8            .getAnnotatedMethods(BeforeClass.class);

 9     return befores.isEmpty() ? statement :

        new RunBefores(statement, befores, null);

 }

 protected Statement methodBlock(FrameworkMethod method) {

     

     Statement statement= methodInvoker(method, test);

     

     statement= withBefores(method, test, statement);

     

 }

 protected Statement withBefores(FrameworkMethod method, Object target,

        Statement statement) {

     List<FrameworkMethod> befores= getTestClass().getAnnotatedMethods(

            Before.class);

     return befores.isEmpty() ? statement : new RunBefores(statement,

            befores, target);

 }

@AfterClass和@After

这两个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 {

 2     private final Statement fNext;

 3     private final Object fTarget;

 4     private final List<FrameworkMethod> fAfters;

 5     public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {

 6        fNext= next;

 7        fAfters= afters;

 8        fTarget= target;

 9     }

     @Override

     public void evaluate() throws Throwable {

        List<Throwable> errors = new ArrayList<Throwable>();

        try {

            fNext.evaluate();

        } catch (Throwable e) {

            errors.add(e);

        } finally {

            for (FrameworkMethod each : fAfters)

               try {

                   each.invokeExplosively(fTarget);

               } catch (Throwable e) {

                   errors.add(e);

               }

        }

        MultipleFailureException.assertEmpty(errors);

     }

 }

从源码中可以看到,构造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)。

 1 protected Statement classBlock(final RunNotifier notifier) {

 2     

 3     statement= withBeforeClasses(statement);

 4     statement= withBeforeClasses(statement);

 5     

 6 }

 7 protected Statement withAfterClasses(Statement statement) {

 8     List<FrameworkMethod> afters= fTestClass

 9            .getAnnotatedMethods(AfterClass.class);

     return afters.isEmpty() ? statement :

        new RunAfters(statement, afters, null);

 }

 protected Statement methodBlock(FrameworkMethod method) {

     

     statement= withBefores(method, test, statement);

     statement= withAfters(method, test, statement);

     

 }

 protected Statement withAfters(FrameworkMethod method, Object target,

        Statement statement) {

     List<FrameworkMethod> afters= getTestClass().getAnnotatedMethods(

            After.class);

     return afters.isEmpty() ? statement : new RunAfters(statement, afters,

            target);

 } 
 

InvokeMethod、ExpectedException和FailOnTimeout

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

 @Retention(RetentionPolicy.RUNTIME)

 @Target({ElementType.METHOD})

 public @interface Test {

     Class<? extends Throwable> expected() default None.class;

     long timeout() default 0L;

 }

@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 {

        fTestMethod.invokeExplosively(fTarget);

     }

 }

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

 1 protected Statement methodBlock(FrameworkMethod method) {

 2     

 3     Statement statement= methodInvoker(method, test);

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

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

 6     

 7 }

 8 protected Statement methodInvoker(FrameworkMethod method, Object test) {

 9     return new InvokeMethod(method, test);

 }

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;

 7     }

 8     @Override

 9     public void evaluate() throws Exception {

        boolean complete = false;

        try {

            fNext.evaluate();

            complete = true;

        } catch (AssumptionViolatedException e) {

            throw e;

        } catch (Throwable e) {

            if (!fExpected.isAssignableFrom(e.getClass())) {

               String message= "Unexpected exception, expected<"

                          + fExpected.getName() + "> but was<"

                          + e.getClass().getName() + ">";

               throw new Exception(message, e);

            }

        }

        if (complete)

            throw new AssertionError("Expected exception: "

                   + fExpected.getName());

     }

 }

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

 1 protected Statement methodBlock(FrameworkMethod method) {

 2     

 3     Statement statement= methodInvoker(method, test);

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

 5     

 6 }

 7 protected Statement possiblyExpectingExceptions(FrameworkMethod method,

 8        Object test, Statement next) {

 9     Test annotation= method.getAnnotation(Test.class);

     return expectsException(annotation) ? new ExpectException(next,

            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;

 7     }

 8     @Override

 9     public void evaluate() throws Throwable {

        StatementThread thread= evaluateStatement();

        if (!thread.fFinished)

            throwExceptionForUnfinishedThread(thread);

     }

     private StatementThread evaluateStatement() throws InterruptedException {

        StatementThread thread= new StatementThread(fOriginalStatement);

        thread.start();

        thread.join(fTimeout);

        thread.interrupt();

        return thread;

     }

     private void throwExceptionForUnfinishedThread(StatementThread thread)

            throws Throwable {

        if (thread.fExceptionThrownByOriginalStatement != null)

            throw thread.fExceptionThrownByOriginalStatement;

        else

            throwTimeoutException(thread);

     }

     private void throwTimeoutException(StatementThread thread) throws Exception {

        Exception exception= new Exception(String.format(

               "test timed out after %d milliseconds", fTimeout));

        exception.setStackTrace(thread.getStackTrace());

        throw exception;

     }

     private static class StatementThread extends Thread {

        private final Statement fStatement;

        private boolean fFinished= false;

        private Throwable fExceptionThrownByOriginalStatement= null;

        public StatementThread(Statement statement) {

            fStatement= statement;

        }

        @Override

        public void run() {

            try {

               fStatement.evaluate();

               fFinished= true;

            } catch (InterruptedException e) {

               //don't log the InterruptedException

            } catch (Throwable e) {

               fExceptionThrownByOriginalStatement= e;

            }

        }

     }

 }

FailOnTimeout的构造过程如同上述的其他Statement类似:

 1 protected Statement methodBlock(FrameworkMethod method) {

 2     

 3     Statement statement= methodInvoker(method, test);

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

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

 6     

 7 }

 8 protected Statement withPotentialTimeout(FrameworkMethod method,

 9        Object test, Statement next) {

     long timeout= getTimeout(method.getAnnotation(Test.class));

  ? new FailOnTimeout(next, timeout) : next;

 }

Fail与RunRules

Fail这个Statement是在创建测试类出错时构造的Statement,这个设计思想有点类似Null Object模式,即保持编程模型的统一,即使在出错的时候也用一个Statement去封装,这也正是Runner中ErrorReportingRunner的设计思想。

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

 1 protected Statement methodBlock(FrameworkMethod method) {

 2     Object test;

 3     try {

 4        test= new ReflectiveCallable() {

 5            @Override

 6            protected Object runReflectiveCall() throws Throwable {

 7               return createTest();

 8            }

 9        }.run();

     } catch (Throwable e) {

        return new Fail(e);

     }

     

 }

 public class Fail extends Statement {

     private final Throwable fError;

     public Fail(Throwable e) {

        fError= e;

     }

     @Override

     public void evaluate() throws Throwable {

        throw fError;

     }

 }

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();

 9     }

     private static Statement applyAll(Statement result, Iterable<TestRule> rules,

            Description description) {

        for (TestRule each : rules)

            result= each.apply(result, description);

        return result;

     }

 }

 protected Statement classBlock(final RunNotifier notifier) {

     

     statement= withAfterClasses(statement);

     statement= withClassRules(statement);

     return statement;

 }

 private Statement withClassRules(Statement statement) {

     List<TestRule> classRules= classRules();

     return classRules.isEmpty() ? statement :

         new RunRules(statement, classRules, getDescription());

 }

 protected Statement methodBlock(FrameworkMethod method) {

     

     statement= withRules(method, test, statement);

     return statement;

 }

 private Statement withRules(FrameworkMethod method, Object target,

        Statement statement) {

     List<TestRule> testRules= getTestClass().getAnnotatedFieldValues(

         target, Rule.class, TestRule.class);

     return testRules.isEmpty() ? statement :

        new RunRules(statement, testRules, describeChild(method));

 }

Rule分为@ClassRule和@Rule,@ClassRule是测试类级别的,它对一个测试类只运行一次,而@Rule是测试方法级别的,它在每个测试方法运行时都会运行。RunRules的构造过程中,我们可以发现RunRules才是最外层的Statement,TestRule中要执行的逻辑要么比@BeforeClass、@Before注解的方法要早,要么比@AfterClass、@After注解的方法要迟。

Statement执行序列图

假设在一个测试类中存在多个@BeforeClass、@AfterClass、@Before、@After注解的方法,并且有两个测试方法testMethod1()和testMethod2(),那么他们的运行序列图如下所示(这里没有考虑Rule,对@ClassRule的RunRules,它的链在最前端,而@Rule的RunRules则是在RunAfters的前面,关于Rule将会在下一节中详细讲解):

junit4X系列--Statement的更多相关文章

  1. junit4X系列--Runner解析

    前面我整理了junit38系列的源码,那junit4X核心代码也基本类似.这里我先转载一些关于junit4X源码解析的好文章.感谢原作者的分享.原文地址:http://www.blogjava.net ...

  2. Junit4X系列--hamcrest的使用

    OK,在前面的一系列博客里面,我整理过了Assert类下面常用的断言方法,比如assertEquals等等,但是org.junit.Assert类下还有一个方法也用来断言,而且更加强大.这就是我们这里 ...

  3. junit4X系列源码--Junit4 Runner以及test case执行顺序和源代码理解

    原文出处:http://www.cnblogs.com/caoyuanzhanlang/p/3534846.html.感谢作者的无私分享. 前一篇文章我们总体介绍了Junit4的用法以及一些简单的测试 ...

  4. junit4X系列--Builder、Request与JUnitCore

    原文出处:http://www.blogjava.net/DLevin/archive/2012/05/12/377957.html.感谢作者的无私分享. 初次用文字的方式记录读源码的过程,不知道怎么 ...

  5. junit4X系列--Rule

    原文出处:http://www.blogjava.net/DLevin/archive/2012/05/12/377955.html.感谢作者的无私分享. 初次用文字的方式记录读源码的过程,不知道怎么 ...

  6. junit4X系列--Assert与Hamcrest

    原文出处:http://www.blogjava.net/DLevin/archive/2012/05/12/377960.html.感谢作者无私分享 到目前,JUnit4所有的核心源码都已经讲解过了 ...

  7. junit4X系列源码--总体介绍

    原文出处:http://www.cnblogs.com/caoyuanzhanlang/p/3530267.html.感谢作者的无私分享. Junit是一个可编写重复测试的简单框架,是基于Xunit架 ...

  8. junit4X系列--Exception

    原文出处:http://www.blogjava.net/DLevin/archive/2012/11/02/390684.html.感谢作者的无私分享. 说来惭愧,虽然之前已经看过JUnit的源码了 ...

  9. junit测试延伸--方法的重复测试

    在实际编码测试中,我们有的时候需要对一个方法进行多次测试,那么怎么办呢?这个问题和测试套件解决的方案一样,我们总不能不停的去右键run as,那怎么办呢?还好伟大的junit帮我们想到了. OK,现在 ...

随机推荐

  1. 初识Hibernate的主配置和映射配置

    Hibernate.cfg.xml 主配置 Hibernate.cfg.xml 主配置文件夹中主要配置:数据库链接配置,其他参数配置,映射信息等. 常用配置查看源码: hibernate-distri ...

  2. MySQL 5.7 InnoDB缓冲池NUMA功能支持——但是别高兴的太早

    当前CPU都已是NUMA架构,相信除了历史遗留系统,很少会有数据库跑在SMP的CPU上了.NUMA架构带来的优势无言而语,CPU更快的内存访问速度,但是带来的问题也不言而喻,特别是对于数据库的影响.M ...

  3. Git的使用-如何将本地项目上传到Github

    默认你的电脑上已经安装了git. 第一步:我们需要先创建一个本地的版本库(其实也就是一个文件夹). 你可以直接右击新建文件夹,也可以右击打开Git bash命令行窗口通过命令来创建. 现在我通过命令行 ...

  4. if与while相互嵌套,菱形*的实现.py

    """    *           * *         * * *       * * * *     * * * * *     * * * *       * ...

  5. NOIP2017游记

    日常大考之前感冒(这次感冒了3周..) Day -4~Day 0 停课一周,不写作业不上课是很爽,然而想到NOIP结束第二天就要期中考.. 在学校刷刷题,跟着一大堆大佬的步伐,做着一大堆大佬的题目(其 ...

  6. JavaScript splice() 方法

    定义和用法 splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目. 注释:该方法会改变原始数组. 例子 1 在本例中,我们将创建一个新数组,并向其添加一个元素: <script ...

  7. JavaSE(十一)之异常处理详解

    一.异常概述 在我们日常生活中,有时会出现各种各样的异常,例如:职工小王开车去上班,在正常情况下,小王会准时到达单位.但是天有不测风云,在小王去上班时,可能会遇到一些异常情况,比如小王的车子出了故障, ...

  8. SPRINGCLOUD 开发学习记录

    一个简单的微服务系统:服务注册和发现,服务消费,负载均衡,断路器,智能路由,配置管理 服务注册中心: eureka是一个高可用组件,没有后端缓存,每一个实例注册后向注册中心发送心跳,默认情况下,eru ...

  9. bzoj 1084;vijos 1191 [SCOI2005] 最大子矩阵

    Description 这里有一个n*m的矩阵,请你选出其中k个子矩阵,使得这个k个子矩阵分值之和最大.注意:选出的k个子矩阵不能相互重叠. Input 第一行为n,m,k(1≤n≤100,1≤m≤2 ...

  10. RAID 详解

    一.什么是RAID 磁盘阵列全名是『Redundant Arrays of Inexpensive Disks, RAID 』,英翻中的意思是:容错式廉价磁盘阵列. RAID 可以透过一个技术(软件或 ...