你知道 Junit 是怎么跑的吗?

Junit 是由 Kent Beck 和 Erich Gamma 于 1995 年末着手编写的框架,自此之后,Junit 框架日益普及,如今已经成为单元测试 Java 应用程序的事实上的标准。
java

在软件开发领域中,历来没有这样的事情:少数几行代码对大量代码起着如此重要的做用 --- Martin Fowler缓存

从一个简单的例子开始认识 Junit

本文注重点在于研究 Junit 运行的基本原理和执行单元测试的流程,因此对于一些额外的信息和数据不单独准备,本文所使用的测试 case 以下:markdown

package com.glmapper.bridge.boot;

import org.junit.*;

public class JunitSamplesTest {

    @Before
    public void before(){
        System.out.println(".....this is before test......");
    }

    @After
    public void after(){
        System.out.println(".....this is after test......");
    }

    @BeforeClass
    public static void beforeClass(){
        System.out.println(".....this is before class test......");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println(".....this is after class test......");
    }

    @Test
    public void testOne(){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(){
        System.out.println("this is test two");
    }
}

复制代码

执行结果以下:app

.....this is before class test...... Disconnected from the target VM, address: '127.0.0.1:65400', transport: 'socket' .....this is before test...... this is test one .....this is after test...... .....this is before test...... this is test two .....this is after test...... .....this is after class test...... 复制代码

从代码和执行结果来看,BeforeClass 和 AfterClass 注解分别在测试类开始以前和以后执行,Before 和 After 注解在测试类中每一个测试方法的先后执行。框架

问题域

从开发者的角度来看,对于任何一个技术产品组件,若是想要更好的使用它,就意味着必须了解它。经过上面提供的 case 能够看到,Junit 使用很是简单,基本 0 门槛上手,经过给测试的方法加一个 @Test 注解,而后将待测试逻辑放在 被 @Test 标注的方法内,而后 run 就行了。简单源于组件开发者的顶层抽象和封装,将技术细节屏蔽,而后以最简洁的 API 或者注解面向用户,这也是 Junit 可以让广大开发者容易接受的根本缘由,值得咱们借鉴学习。socket

回归正题,基于上面分析,Junit 使用简单在于其提供了很是简洁的 API 和注解,那对于咱们来讲,这些就是做为分析 Junit 的基本着手点;经过这些,来拨开 Junit 的基本原理。基于第一节的小案例,这里抛出这样几个问题:ide

  • Junit 是怎么触发执行的
  • 为何被标注 @Test 注解的方法会被执行,而没有标注的不会
  • Before 和 After 执行时机
  • BeforeClass 和 AfterClass 执行时机
  • Junit 是怎么将执行结果收集并返回的(这里不关注 IDE 提供的渲染)

Junit 是如何执行的?

这里把断点直接打在目标测试方法位置,而后 debug 执行
image.png函数

经过堆栈来找到用例执行的整个路径。由于本 case 是经过 idea 启动执行,因此能够看到的入口实际是被 idea 包装过的。可是这里也抓到了 JUnitCore 这样的一个入口。单元测试

JUnitCore 是运行测试用例的门面入口,经过源码注释能够看到,JUnitCore 从 junit 4 才有,可是其向下兼容了 3.8.x 版本系列。咱们在跑测试用例时,其实大多数状况下在本地都是经过 IDE 来触发用例运行,或者经过 mvn test 来运行用例,实际上,不论是 IDE 仍是 mvn 都是对 JUnitCore 的封装。咱们彻底能够经过 main 方法的方式来运行,好比运行下面代码的 main 方法来经过一个 JUnitCore 实例,而后指定被测试类来触发用例执行,为了尽可能使得堆栈更贴近 Junit 本身的代码,咱们经过这种方式启动来减小堆栈对于代码执行路径的干扰。学习

public class JunitSamplesTest {

    @Before
    public void before(){
        System.out.println(".....this is before test......");
    }

    @After
    public void after(){
        System.out.println(".....this is after test......");
    }

    @BeforeClass
    public static void beforeClass(){
        System.out.println(".....this is before class test......");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println(".....this is after class test......");
    }

    @Test
    public void testOne(){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(){
        System.out.println("this is test two");
    }

    public static void main(String[] args) {
        JUnitCore jUnitCore = new JUnitCore();
        jUnitCore.run(JunitSamplesTest.class);
    }
}

复制代码

这里获得了最简化的测试执行入口:

image.png

若是使用 java 命令来引导启动,其实就是从 JunitCore 内部本身的 main 方法开始执行的

/** * Run the tests contained in the classes named in the args. If all tests run successfully, exit with a status of 0. Otherwise exit with a status of 1. Write * feedback while tests are running and write stack traces for all failed tests after the tests all complete. * Params: * args – names of classes in which to find tests to run **/

public static void main(String... args) {
    Result result = new JUnitCore().runMain(new RealSystem(), args);
    System.exit(result.wasSuccessful() ? 0 : 1);
}

复制代码

为何被标注 @Test 注解的方法会被执行,而没有标注的不会

这里比较好理解,被打了 @Test 注解的方法,必定是 Junit 经过某种方式将其扫描到了,而后做为待执行的一个集合或者队列中。下面经过分析代码来论证下。

org.junit.runners.BlockJUnit4ClassRunner#getChildren

@Override
protected List<FrameworkMethod> getChildren() {
    return computeTestMethods();
}
复制代码

经过方法 computeTestMethods 方法名其实就能够看出其目的,就是计算出全部的测试方法。

image.png

etAnnotatedMethods 经过指定的 annotationClass 类型,将当前 TestClass 中类型为 annotationClass 类型注解标注的方法过滤出来,

image.png

getFilteredChildren 中最后将获取获得的测试方法放在 filteredChildren 中缓存起来。这里简单汇总下 @Test 注解被识别的整个过程(其余注解如 @Before 都是同样的)

  • 一、Junit 在初始化构建 Runner 的过程,内部会基于给定的 测试类建立一个 TestClass 对象模型,用于描述当前测试类在 Junit 中的表示。
// clazz 是待测试类
public TestClass(Class<?> clazz) {
    this.clazz = clazz;
    if (clazz != null && clazz.getConstructors().length > 1) {
        // 测试类不能有有参构造函数
        throw new IllegalArgumentException(
            "Test class can only have one constructor");
    }

    Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkMethod>>();
    Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkField>>();
    // 扫描待测试类中全部的 Junit 注解,包括 @Test @Before @After 等等
    scanAnnotatedMembers(methodsForAnnotations, fieldsForAnnotations);
	// 过滤出打在方法上的注解,
    this.methodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
    // 过滤出打在变量上的注解
    this.fieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
}
复制代码

methodsForAnnotations 和 fieldsForAnnotations 缓存了当前待测试类全部被 junit 注解标注过的方法和变量

  • 二、getFilteredChildren 中,从 methodsForAnnotations 中筛选出全部 @Test 注解标注的方法。(getDescription()-> getFilteredChildren -> computeTestMethods -> 从 methodsForAnnotations 按类型过滤)
  • 三、返回全部 @Test 注解标注的方法

Before 和 After 执行时机

要搞定这个问题,其实有必要了解下 Junit 中一个比较重要的概念 Statement。

public abstract class Statement {
    /** * Run the action, throwing a {@code Throwable} if anything goes wrong. */
    public abstract void evaluate() throws Throwable;
}
复制代码

Statement 从 junit 4.5 版本被提出,Statement 表示在运行 JUnit 测试组件的过程当中要在运行时执行的一个或多个操做,简单说就是,对于被 @Before @After 注解标注的方法,在 JUnit 会被做为一种 Statement 存在,分别对应于 RunBefores 和 RunnerAfter,这些 statement 中持有了当前运行全部的 FrameworkMethod。

FrameworkMethod 是 JUnit 中全部被 junit 注解标注方式的内部描述,@Test, @Before, @After, @BeforeClass, @AfterClass 标注的方法最终都做为 FrameworkMethod 实例存在。

Statement 的建立有两种方式,基于 FrameworkMethod 的 methodBlock 和基于 RunNotifier 的 classBlock,这里介绍 methodBlock ,classBlock 下节讨论。

protected Statement methodBlock(final FrameworkMethod method) {
        Object test;
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest(method);
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement = methodInvoker(method, test);
        statement = possiblyExpectingExceptions(method, test, statement);
        statement = withPotentialTimeout(method, test, statement);
        statement = withBefores(method, test, statement);
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        statement = withInterruptIsolation(statement);
        return statement;
    }
复制代码

withAfters、withBefores 会将 RunAfters 和 RunBefore 绑定到 statement,最后 造成一个 statement 链,这个链的执行入口时 RunAfters#evaluate。

@Override
public void evaluate() throws Throwable {
    List<Throwable> errors = new ArrayList<Throwable>();
    try {
        next.evaluate();
    } catch (Throwable e) {
        errors.add(e);
    } finally {
        // 在 finally 中执行 after 方法
        for (FrameworkMethod each : afters) {
            try {
                invokeMethod(each);
            } catch (Throwable e) {
                errors.add(e);
            }
        }
    }
    MultipleFailureException.assertEmpty(errors);
}
复制代码

next 链中包括 before 和待执行的测试方法

image.png

因此咱们看到的就是 before -> testMethod -> after。

这里其实和预想的不太同样,关于 before 和 after 这种逻辑,第一想法是经过代理的方式,对测试方法进行代理拦截,相似 Spring AOP 中的 Before 和 After,其实否则。

BeforeClass 和 AfterClass 执行时机

前面分析了 methodBlock,了解到 junit 中经过这个方法建立 statement 而且将 before 和 after 的方法绑定给 statement,以此推断,classBlock 的做用就是将 BeforeClass 和 AfterClass 绑定给statement 。

protected Statement classBlock(final RunNotifier notifier) {
    // childrenInvoker 这里会调用到 methodBlock
    Statement statement = childrenInvoker(notifier);
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        statement = withInterruptIsolation(statement);
    }
    return statement;
}
复制代码

BeforeClass 和 before 都会对应建立一个 RunnerBefores,区别在于 BeforeClass 在建立 RunnerBefores 时,不会指定目标测试方法。

  • BeforeClass 在执行 statement 以前,运行该类和超类上全部非覆盖的@BeforeClass方法;若是有抛出异常,中止执行并传递异常。
  • AfterClass 在执行 statement链最后,在该类和超类上运行全部未覆盖的 @AfterClass 方法;始终执行全部 AfterClass 方法:若有必要,将前面步骤抛出的异常与来自 AfterClass 方法的异常合并到 org.junit.runners.model.MultipleFailureException 中。

Junit 是怎么将执行结果收集并返回的

junit 全部执行的结果都存放在 Result 中

// 全部 case 数
private final AtomicInteger count;
// 忽略执行的 case 数(被打了 ignore)
private final AtomicInteger ignoreCount;
// 失败 case 数
private final AtomicInteger assumptionFailureCount;
// 全部失败 case 的结果
private final CopyOnWriteArrayList<Failure> failures;
// 执行时间
private final AtomicLong runTime;
// 开始时间
private final AtomicLong startTime;
复制代码

Result 中内置了一个默认的来监听器,这个监听器会在每一个 case 执行完成以后进行相应的回调,Listener 以下:

@RunListener.ThreadSafe
    private class Listener extends RunListener {
    // 设置开始时间
        @Override
        public void testRunStarted(Description description) throws Exception {
            startTime.set(System.currentTimeMillis());
        }
		
        // 执行完全部 case
        @Override
        public void testRunFinished(Result result) throws Exception {
            long endTime = System.currentTimeMillis();
            runTime.addAndGet(endTime - startTime.get());
        }
		// 执行完某个 case
        @Override
        public void testFinished(Description description) throws Exception {
            count.getAndIncrement();
        }
		// 执行完某个 case 失败
        @Override
        public void testFailure(Failure failure) throws Exception {
            failures.add(failure);
        }
		// 执行完某个ignore case
        @Override
        public void testIgnored(Description description) throws Exception {
            ignoreCount.getAndIncrement();
        }

        @Override
        public void testAssumptionFailure(Failure failure) {
        // Assumption 产生的失败
            assumptionFailureCount.getAndIncrement();
        }
    }
复制代码

JUnit 4 开始在测试中支持假设 Assumptions,在 Assumptions 中,封装了一组使用的方法,以支持基于假设的条件测试执行。假设实际就是指定某个特定条件,假如不能知足假设条件,假设不会致使测试失败,只是终止当前测试。这也是假设与断言的最大区别,由于对于断言而言,会致使测试失败。

因此 JUnit 经过监听器机制收集全部的测试信息,最终封装到 Result 中返回。

总结

Junit 中有一些比较基本的概念,好比 Runner,statement 等;在初始化时,默认状况下 junit 会构建出 BlockJUnit4ClassRunner 这样的一个 Runner,而且在这个 Runner 中会持有被测试类的全部信息。Runner 运行测试并在执行此操做时将重要事件通知 RunNotifier。

也可使用 RunWith 调用自定义 Runner,这里只要你的 Runner 是 org.junit.runner.Runner 子类便可;建立自定义运行程序时,除了在此处实现抽象方法外,还必须提供一个构造函数,这个构造函数将包含测试的类做为参数--如:SpringRunner。

Runner 的 run 方法内部就是构建和执行 Statement 链的过程,Statement 中描述了单元测试中须要执行的一系列操做,每一个 case 均以 RunnerAfter -> TargetMethod -> RunnerBefore 的执行顺序依次执行;执行过程当中,junit 经过监听器机制回调 case 调用的每一个生命周期阶段,并将各个case 执行的信息进行收集汇总,最终返回执行结果 Result 。