高手的存在,就是让服务10亿人的时候,你感觉只是为你一个人服务......

Junit4的执行流程

目录
  1. 1. 为何选择了BlockJUnit4ClassRunner执行器
  2. 2. BlockJUnit4ClassRunner中发生了什么
  • 总结
  • Junit作为一款优秀的Java单元测试框架,在工作中经常会用到,利用JUnit进行单元测试非常简单方便。

    我们平时在写的NGrinder性能测试脚本也是依托于Junit,所以熟悉Junit的执行流程很有必要。


    通过一段简单的测试代码来分析Junit4的执行流程。

    添加Junit依赖,这里使用junit4.12版本。

    1
    2
    3
    4
    5
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    </dependency>

    测试代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.junit.Test;
    import static org.junit.Assert.assertTrue;

    public class testA {
    @Test
    public void doTest() {
    System.out.println("junit4 dotest");
    assertTrue(true);
    }
    }

    我们通过IDEA进行debug,这样能比较方便的看到Junit的执行流程。

    IDEA使用的IntelliJ IDEA 2017.3.5 (Ultimate Edition)版本,内部默认安装了Junit执行的相关插件。
    Alt text

    在测试代码上打断点,进行debug。

    Alt text

    Alt text

    Alt text

    从debugger上可以看到,测试的JVM进程由com.intellij.rt.execution.junit.JUnitStarter.main()方法启动,这个类是IDEA JUnit插件的,真正进入到JUnit的代码是org.junit.runner.JUnitCore.run(),最终交由BlockJUnit4ClassRunner执行器来执行测试。


    为何选择了BlockJUnit4ClassRunner执行器

    Junit的执行器(runner)很多,在Junit4.4之前默认的是Junit4ClassRunner,JUnit4.4之后默认使用BlockJUnit4ClassRunner。

    当然我们也可以通过@RunWith注解来指定执行器,比如Spring项目的单元测试经常使用@RunWith(SpringJUnit4ClassRunner.class)。NGrinder脚本中使用的GrinderRunner执行器@RunWith(GrinderRunner)。

    这里我们使用默认的执行器BlockJUnit4ClassRunner来分析执行流程,先来看看Junit是如何选择BlockJUnit4ClassRunner执行器。

    在ClassRequest#getRunner()入口方法打上断点,执行测试。跟着代码一步一步调试。
    Alt text

    一直执行到AllDefaultPossibilitiesBuilder#runnerForClass(Class<?> testClass)方法。

    Alt text

    可以看到,执行测试时通过AllDefaultPossibilitiesBuilder类会找到合适的执行器(runner)。

    runnerForClass方法中给出了5种RunnerBuilder,它们会被按顺序依次遍历,找到一个合适的Runner后即停止:

    IgnoreBuilder。检查被测类是否含有@Ignore注解,如果有,则初始化一个IgnoredClassRunner,否则返回null。

    AnnotatedBuilder。检查被测类是否含有@RunWith注解,如果有,则用该注解的value初始化一个Runner,否则返回null。

    SuiteMethodBuilder。检查被测类是否含有一个叫“suite”的方法,如果有,则初始化一个SuiteMethod(这是JUnit3.8中使用的Runner),否则返回null。

    JUnit3Builder。检查被测类是否是TestCase的子类,如果是,则初始化一个JUnit38ClassRunner,否则返回null。

    JUnit4Builder。没有检查条件。将初始化一个BlockJUnint4ClassRunner。这也是JUnit4默认的Runner。

    经过这5步,必然会找到一个Runner,我们这个了例子就会返回BlockJUnit4ClassRunner


    BlockJUnit4ClassRunner中发生了什么

    确认了BlockJUnit4ClassRunner执行器后,才真正进入正题。

    继续debug,跟着代码一步一步走,回到JUnitCore类,一直到run(Runner runner)方法。

    接下来是关键,到了测试方法被真正执行的地方。

    Alt text

    在方法内部调用到了Runner的run(notifier)方法,这里的Runner就是BlockJUnit4ClassRunner。它的run方法在其父类ParentRunner类中实现。

    Alt text

    这段代码是关键的地方:

    1
    2
    Statement statement = classBlock(notifier);
    statement.evaluate();

    classBlock方法返回了包含测试执行单元的Statement,然后通过statement.evaluate方法执行我们的测试类。

    先来看下classBlock方法内部:
    Alt text

    这段代码的关键在childrenInvoker(notifier)方法,通过反射获得初始Statement,然后附加上RunBefore、RunAfter、用户自定义Rule。返回的Statement是一个可执行runChildren方法的Statement,后面的statement.evalueate()方法会回调runChildren方法。

    我们继续debug进入statement.evalueate()方法内部,一直到runChild()执行。

    仔细看看BlockJUnit4ClassRunner#runChild()的实现,这个方法是最终执行测试的地方。

    Alt text

    可以看到,如果测试用例上标注了@Ignore注解,则不会执行。否则会调用runLeaf方法来执行测试。

    上面说了这么多,终于到了最后一步runLeaf。

    runLeaf方法中第一个入参methodBlock(method),跟上面的classBlock类似,也是一个处理测试方法的函数,返回更具体的Statement对象。

    Alt text

    先调用methodInvoker创建了一个InvokeMethod(Statement的子类)对象。

    如果该用例用@Test(expected=”xxx”)标注,则用statement组装并返回一个ExpectException(Statement的子类)对象。

    如果该用例用@Test(timeout=”xxx”)标注,则用statement组装并返回一个FailOnTimeout(Statement的子类)对象。

    如果该测试类中有方法用@Before标记,则用statement组装并返回一个RunBefores(Statement的子类)对象。

    如果该测试类中有方法用@After标记,则用statement组装并返回一个RunAfters(Statement的子类)对象。

    如果该用例上有@Rule,则用statement组装并返回一个RunRules(Statement的子类)对象。

    Statement是一个抽象类,这里提到的InvokeMethod,ExpectException,FailOnTimeout,RunBefores,RunAfters,RunRules这些对象都是Statement类的子类,并且都实现了evaluate方法。

    接下来就是发出调用执行了,我们看runLeaf方法:

    Alt text

    runLeaf方法会调用statement对象的evaluate方法,statement分成数种职责(BeforeClass, AfterClass,Before,After,测试方法等),不同的Statement负责自己的职责,执行结束后交给下一个Statement,直到所有Statement执行完毕。这也是我们所说的职责链模式(Chain Of Responsibility Pattern)。


    总结

    用一句话来总结整个执行流程:找到执行器BlockJUnit4ClassRunner,找到(BeforeClass, AfterClass,Before,After,测试方法等)Statement,依次执行evaluate。


    参考资料:
    https://my.oschina.net/itblog/blog/1550931
    https://www.jianshu.com/p/ad524e211ef3