目录

  • 背景
  • JUnit 测试在 IntelliJ 中的执行
  • 阶段 0:IntelliJ 部分
  • 阶段 1:JUnitCore.run()
  • 阶段 2:ParentRunner.run()
  • 阶段 3:ParentRunner.runChildren()
  • 阶段 4:BlockJUnit4ClassRunner.runChild()
  • 阶段 5:InvokeMethod.evaluate()
  • 小结


背景

继上一个系列初步研究了单元测试神器 Mockito 的实现原理之后,这个系列关注一下单元测试框架本身:JUnit。

相信大家对于在 IDE 中执行单元测试毫不陌生。除了单元测试,我也经常会用 JUnit 验证一些 Java 的语法机制,毕竟 Java 不是脚本语言,总得有个入口才能执行程序呀!总不能每次都新建一个 project 吧。如此说来,JUnit 真的是一个非常方便的工具。

下面就来看看,当你点击 IntelliJ 中的“执行测试”的按钮时,JUnit 中到底发生了什么。

(本文基于 JUnit 4.13.2 版本)

JUnit 测试在 IntelliJ 中的执行

以下面一个非常简单的 test 为例:

import static org.junit.Assert.assertEquals;

public class MyTest {
    @Test
    public void simpleTest() {
        assertEquals(1, 1); // 在这里加了断点
    }
}

如上所示,我在测试代码上打了断点,通过观察代码执行到这一行时的调用栈,应该就可以大概摸清楚 JUnit 的工作原理。

阶段 0:IntelliJ 部分

你点击的是 IntelliJ 中的按钮,对不对?所以程序的执行一定是从 IntelliJ 开始的,这时候 JUnit 对于你要执行测试这件事还毫不知情!

androidstudio junit框架单元测试示例_java

如果看一下调用栈,会发现前 4 个调用都在 IntelliJ 部分,从第 5 个调用开始才进入 JUnit 的 JUnitCore

androidstudio junit框架单元测试示例_lua_02


这部分的源码是看不到的,但大概也能猜出来干了啥,就是采集关于你要执行什么测试的信息,然后把这些信息通过 Runner 对象来传递给 JUnitCore ——也就是下面要讨论的部分——来执行测试。

阶段 1:JUnitCore.run()

很有意思,接下来进入到了 JUnitCore 的一个被标注为 “Do not use” 的 run 方法中:

// org/junit/runner/JUnitCore.java
// 第 128 行
/**
 * Do not use. Testing purposes only.
 */
public Result run(Runner runner) {
    Result result = new Result();
    RunListener listener = result.createListener();
    notifier.addFirstListener(listener);
    try {
        notifier.fireTestRunStarted(runner.getDescription()); // 记录测试开始
		// 执行测试
        runner.run(notifier);
        notifier.fireTestRunFinished(result); // 记录测试结束
    } finally {
        removeListener(listener);
    }
    return result;
}

看来 JUnit 的人不希望别人调用这个方法,但 IntelliJ 还是调用了。。

IntelliJ 为什么要调用这个方法呢?当然是为了执行测试。和执行测试相关的信息都被放到 runner 这个对象中了(比如你执行的是哪个测试类,或者哪个测试方法,等等),也就是说执行它就可以了。

但这一阶段的代码并不负责执行测试本身,而是做一件额外的事情:信息记录。

在测试执行的各个阶段都会产生信息。比如,测试的开始时间?测试是否通过?测试的结束时间?这些信息都需要记录,但由于这些信息分别产生于不同阶段,所以要想把它们统一收集到一个地方就会比较麻烦。而我们的确是需要把它们统一收集到一个地方的(比如,统一传回给 IDE 进行 UI 展示)。

这个时候,“观察者模式” 就派上用场了。可以看到,方法的第一行首先首先创建了一个 Result 类的对象 result,这个就是用来收集所有测试相关信息的对象!

紧接着让 result 创建一个 listener 对象,并“挂载”到 notifier (这是一个类成员)对象。在测试开始前和结束后分别执行 notifier.fireTestRunStartednotifier.fireTestRunFinished 方法,这样相当于确保把“测试开始”和“测试结束”的相关信息收集到 result 中了!

而在真正执行测试的那行:

runner.run(notifier)

notifier 也被作为参数传进去了。可以想见,测试执行过程中发生了重要事件,都会调用 notifier 的某个方法;而又因为 resultnotifier 的 listener,所以这些信息就被顺利收集到 result 对象中了!

再来看这段代码最后:从notifier 中移除 result 这个 listener,然后返回 result 对象。很合理!因为信息已经收集完毕,作为结果返回就行了。

阶段 2:ParentRunner.run()

上一阶段主要负责的是信息收集逻辑。真正触发测试执行的是其中的 runner.run(notifier) 那一行。

正是通过这一行,进入到了第二个阶段:ParentRunner。因为这里的 runner(即 IntelliJ 传过来的包含所有执行测试所需信息的对象)就是一个 ParentRunner 类的实例。

来看一下这部分的代码:

// org/junit/runners/ParentRunner.java
// 第 406 行
@Override
public void run(final RunNotifier notifier) {
    EachTestNotifier testNotifier = new EachTestNotifier(notifier,
            getDescription());
    testNotifier.fireTestSuiteStarted();
    try {
    	// 确定测试执行方案
        Statement statement = classBlock(notifier);
        // 执行测试
        statement.evaluate();
    } catch (AssumptionViolatedException e) {
        testNotifier.addFailedAssumption(e);
    } catch (StoppedByUserException e) {
        throw e;
    } catch (Throwable e) {
        testNotifier.addFailure(e);
    } finally {
        testNotifier.fireTestSuiteFinished();
    }
}

先来看一下第一行,构建了一个 testNotifier。可以把它大体看做 “装饰模式”,是对 notifier 的装饰。它仍然是基于 notifier 的,用 testNotifier 触发事件实际上相当于用 notifier 触发事件。所以,可以看到这个方法内在很多地方都在用 testNotifier 触发事件,其实本质上还是在用 notifier 收集各个测试阶段的信息,统一收集到上面提到的 result 对象中。

实际上,除掉 notifier 相关的逻辑,这个方法里只剩下 2 行代码:

// 确定测试执行方案
Statement statement = classBlock(notifier);
// 执行测试
statement.evaluate();

这里的 Statement 是一个抽象类,定义如下:

// org/junit/runners/model/Statement.java
public abstract class Statement {
	/**
	 * Run the action, throwing a {@code Throwable} if anything goes wrong.
	 */
	public abstract void evaluate() throws Throwable;
}

后面会看到,Statement 其实很好地体现了 “组合模式”:每个 Statement 都是一个可执行的组件,在执行它的时候,可能会触发更多子组件(同样是 Statement 实例)的执行。例如,当你执行一个测试类的时候,其下所有测试方法都会被执行、所有 @Before@After 注解方法也会执行。

但是当你调用 statement.evaluate() 的时候,不必关心它究竟是一个 “组合” 还是一个单体。这样就把所有可执行的组件一视同仁,用相同的方法去处理,而不必关心它背后的结构是复杂还是简单。

好了,下面来看一下第一行:

// 确定测试执行方案
Statement statement = classBlock(notifier);

classBlock 这个方法之所以要传入 notifier,不是因为需要它来确定测试执行方案,而是用它来收集相关信息。我们当前是在 ParentRunner 类的 runner 实例中,执行测试所需信息都已经存在于这个实例中了,不需要任何额外信息。

classBlock 的代码可以说是比较关键的:

// org/junit/runners/ParentRunner.java
// 第 212 行
protected Statement classBlock(final RunNotifier notifier) {
    Statement statement = childrenInvoker(notifier); // 注意这一行,我们的 MyTest.simpleTest() 就在这里面 
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        statement = withInterruptIsolation(statement);
    }
    return statement;
}

需要注意的是,这个方法只是安排测试的执行,并没有实际执行测试。

“安排”的结果就体现在 statement 对象中。这个方法返回的 statement 对象很可能是一个结构异常复杂的对象。

通过依次查看 childrenInvokerwithBeforeClasseswithAfterClasseswithClassRuleswithInterruptIsolation 的代码,可以看到,这里处理了 JUnit 中几个重要逻辑:

  • childrenInvoker:依次安排所有子测试的执行(比如一个测试类下的所有 @Test 方法,包括我们的 MyTest.simpleTest());
  • withBeforeClasses:在执行子测试之前,安排执行所有 @BeforeClass 方法;
  • withAfterClasses:在执行子测试之后,安排执行所有 @AfterClass 方法;
  • withClassRules:依次应用所有 @ClassRule 字段或方法对应的 rule(应用的结果是修改 statement ,即对执行测试的安排);
  • withInterruptIsolation:无论测试执行中是否出现异常,最终都安排调用 Thread.interrupted()

这几个方法的逻辑都不复杂,很好地体现了 “组合模式” 的优点(把复杂度隐藏在简单的类型背后)。

测试安排好了,下面该执行了吧:

// 执行测试
statement.evaluate();

这一行代码,就会按顺序执行前面安排好的测试了。

首先会走到这一段代码:

// org/junit/runners/ParentRunner.java
// 第 301 行
protected final Statement withInterruptIsolation(final Statement statement) {
    return new Statement() {
        @Override
        public void evaluate() throws Throwable { // 从这里进入
            try {
                statement.evaluate(); // 会走到这里
            } finally {
                Thread.interrupted();
            }
        }
    };
}

这是因为,上面也提到了,statement = withInterruptIsolation(statement); 为我们的 statement 包裹上了一层 try..finally..,这样无论测试执行中是否出现异常,最终都会调用 Thread.interrupted()

而从这里来算,离我们真正要执行的测试 MyTest.simpleTest() 就不远了。因为我们并没有 @BeforeClass 注解,所以直接略过了该步骤,开始执行测试本身了,也就进入了下一个阶段。

阶段 3:ParentRunner.runChildren()

还记得前面提到的 Statement statement = childrenInvoker(notifier); 这一行代码吗,现在我们进来了:

// org/junit/runners/ParentRunner.java
// 第 289 行
protected Statement childrenInvoker(final RunNotifier notifier) {
    return new Statement() {
        @Override
        public void evaluate() {
            runChildren(notifier); // 就是这里
        }
    };
}

然后就走到了这一阶段的关键:

// org/junit/runners/ParentRunner.java
// 第 325 行
private void runChildren(final RunNotifier notifier) {
    final RunnerScheduler currentScheduler = scheduler;
    try {
        for (final T each : getFilteredChildren()) { // 遍历 child
            currentScheduler.schedule(new Runnable() { // 执行该 child
                public void run() {
                    ParentRunner.this.runChild(each, notifier);
                }
            });
        }
    } finally {
        currentScheduler.finished();
    }
}

可以看到,这个阶段只做了两件事:

  • 遍历 child;
  • 使用 currentScheduler 来调度执行每个 child;

事实上,第二件事没有看起来那么复杂。这个 RunnerScheduler 接口的本意是加一层抽象,这样就能更灵活地支持除了顺序执行以外的其他调度方式——比如并行执行。

RunnerScheduler 是 2009 年加上来的,但直到十三年之后的今天,仍然没有除了顺序执行以外的实现。

也就是说,这个 currentScheduler 所做的事情其实就是立刻执行而已。。甚至连 finally{}currentScheduler.finished(); 方法也是个空方法。

上面的代码等价于如下版本:

private void runChildren(final RunNotifier notifier) {
    for (final T each : getFilteredChildren()) { // 遍历 child
    	ParentRunner.this.runChild(each, notifier); // 执行该 child
    }
}

哈哈哈哈哈哈哈哈!是不是简洁了很多?

阶段 4:BlockJUnit4ClassRunner.runChild()

上面的代码自然把我们带到了 runChild() 方法里:

// org/junit/runners/BlockJUnit4ClassRunner.java
// 第 91 行
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
    Description description = describeChild(method);
    if (isIgnored(method)) {
        notifier.fireTestIgnored(description);
    } else {
        Statement statement = new Statement() {
            @Override
            public void evaluate() throws Throwable {
                methodBlock(method).evaluate(); // 然后走到这里,执行测试!
            }
        };
        runLeaf(statement, description, notifier); // 会先走这里
    }
}

值得一提的是,这里已经不是 ParentRunner 了(ParentRunner 是一个抽象类),而是一个派生类 BlockJUnit4ClassRunner。这是 JUnit 默认使用的派生类,没有什么特别之处。

可以看到,这段代码也没做太多事。首先是判断该测试方法是否带有 @Ignore 注解,如果有的话就记录相应信息,然后什么也不做;如果没有的话(正常情况),就走到 runLeaf() 方法中。runLeaf() 也不过是用 notifier 采集一些信息,包括各种异常情况的信息收集,没有什么特别的逻辑,这里就不放上来了。

然后就会走到上面标注的这行:

methodBlock(method).evaluate(); // 执行测试!

还记不记得前面在 “阶段 2:ParentRunner.run()” 中,有一个 classBlock() 方法?这里的 methodBlock() 非常类似,只不过是针对测试方法而不是测试类的:

// org/junit/runners/BlockJUnit4ClassRunner.java
// 第 303 行
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;
}

先来看一下前半部分:

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

这一部分的目的是构造一个测试类的实例。为什么呢?因为每个 @Test 测试都是一个方法,对吧?方法不能凭空执行啊(又不是 static 方法),所以需要在某个具体实例上执行。

比如,我这里执行的是 MyTest.simpleTest() 方法,所以实际上 JUnit 会先创建出来一个 MyTest 的实例 test,然后在这个实例上执行该方法,即 test.simpleTest()

也就是说,对于同一个测试类下的每个测试方法,JUnit 都会新创建一个实例来执行!这样有一个好处,就是你可以初始化一些测试类的成员变量,而它们不会被各个测试方法共享(我就看到过这种用法,当时还奇怪,不同测试方法之间难道不会相互影响吗?后来查了才知道并不会,因为每个测试方法都运行在不同的实例上,起到相互隔离的作用)。

回到我们的代码,其中 ReflectiveCallable 的部分可能看起来有点儿晕,但那其实只是为了解决抛出 InvocationTargetException 异常时的小烦恼。如果忽略掉这一层面的逻辑,上述代码就可以简化成下面这样了:

Object test;
try {
    test = createTest(method);
} 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;

这部分代码和前面的 classBlock 实在是太像了,不需要过多解释应该也能明白了:

  • methodInvoker:安排测试方法的执行;
  • possiblyExpectingExceptions:有些异常是测试所预期抛出的(可以在 @Test 注解上加 expected 参数),这里处理这种情况;
  • withPotentialTimeout:处理超时情况(@Test 注解的 timeout 参数);
  • withBefores:在测试方法执行前,安排执行 @Before 注解的方法;
  • withAfters:在测试方法执行后,安排执行 @After 注解的方法;
  • withRules:依次应用所有 @Rule 字段或方法对应的 rule(应用的结果是修改 statement ,即对执行测试的安排);
  • withInterruptIsolation:无论测试执行中是否出现异常,最终都安排调用 Thread.interrupted()

当然,我们这里的 MyTest 测试类非常简单,没有任何 @Before@BeforeClass@After@AfterClass@Rule 或者 @ClassRule,只需要关注 simpleTest() 测试方法本身的执行就可以了!

对应的就是上面的 methodInvoker

// org/junit/runners/BlockJUnit4ClassRunner.java
// 第 333 行
protected Statement methodInvoker(FrameworkMethod method, Object test) {
    return new InvokeMethod(method, test);
}

methodInvoker 返回的是一个 Statement,最终会被执行 evaluate(),也就来到了下一阶段。

阶段 5:InvokeMethod.evaluate()

至此,终于要执行我们的测试方法本身了!

// org/junit/internal/runners/statements/InvokeMethod.java
public class InvokeMethod extends Statement {
    private final FrameworkMethod testMethod;
    private final Object target;

    public InvokeMethod(FrameworkMethod testMethod, Object target) {
        this.testMethod = testMethod;
        this.target = target;
    }

    @Override
    public void evaluate() throws Throwable {
        testMethod.invokeExplosively(target); // 就是这里!
    }
}

不知道为什么要起这么一个奇怪的名字 invokeExplosively。。但总之,这里就是要执行我们的 simpleTest() 测试啦!来看下 invokeExplosively 的代码:

// org/junit/runners/model/FrameworkMethod.java
public Object invokeExplosively(final Object target, final Object... params)
        throws Throwable {
    return new ReflectiveCallable() {
        @Override
        protected Object runReflectiveCall() throws Throwable {
            return method.invoke(target, params);
        }
    }.run();
}

前面提到过,ReflectiveCallable 只是为了解决抛出 InvocationTargetException 异常时的小烦恼。如果忽略掉这一层面的逻辑,上述代码就可以简化成下面这样了:

public Object invokeExplosively(final Object target, final Object... params)
        throws Throwable {
    return method.invoke(target, params);
}

其实就是执行我们的 simpleTest() 测试方法而已!其中:

  • method 就是我们的 simpleTest,这里用 Java 反射来执行该方法;
  • target 就是前面提到的专门为执行该方法创建的 MyTest 实例;
  • params 本来应该是执行方法的参数,但这里为空;

然后就到我们打的断点了!

小结

JUnit 只是一个执行单元测试的框架,没有什么深奥的原理。理论上来说,你我都能写出这样一个框架。

但不得不说,即便只是实现这样并不深奥的功能,JUnit 的代码组织也非常精巧,值得学习:

首先,不同层面的逻辑被拆分到了不同地方,这样它们之间就可以互不干扰,有非常清晰的边界;

其次,组合模式的使用(也就是那个 Statement 接口)很好地封装了复杂性。

之后有机会的话,会继续探索 JUnit 源码。