目录
- 背景
- 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 对于你要执行测试这件事还毫不知情!

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

这部分的源码是看不到的,但大概也能猜出来干了啥,就是采集关于你要执行什么测试的信息,然后把这些信息通过 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.fireTestRunStarted 和 notifier.fireTestRunFinished 方法,这样相当于确保把“测试开始”和“测试结束”的相关信息收集到 result 中了!
而在真正执行测试的那行:
runner.run(notifier)notifier 也被作为参数传进去了。可以想见,测试执行过程中发生了重要事件,都会调用 notifier 的某个方法;而又因为 result 是 notifier 的 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 对象很可能是一个结构异常复杂的对象。
通过依次查看 childrenInvoker、withBeforeClasses、withAfterClasses、withClassRules 和 withInterruptIsolation 的代码,可以看到,这里处理了 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 源码。
















