0


JUnit 实现原理(1):测试是如何跑起来的

目录

背景

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

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

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

(本文基于 JUnit 4.13.2 版本)

JUnit 测试在 IntelliJ 中的执行

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

importstaticorg.junit.Assert.assertEquals;publicclassMyTest{@TestpublicvoidsimpleTest(){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.
 */publicResultrun(Runner runner){Result result =newResult();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 行@Overridepublicvoidrun(finalRunNotifier notifier){EachTestNotifier testNotifier =newEachTestNotifier(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.javapublicabstractclassStatement{/**
     * Run the action, throwing a {@code Throwable} if anything goes wrong.
     */publicabstractvoidevaluate()throwsThrowable;}

后面会看到,

Statement

其实很好地体现了 “组合模式”:每个

Statement

都是一个可执行的组件,在执行它的时候,可能会触发更多子组件(同样是

Statement

实例)的执行。例如,当你执行一个测试类的时候,其下所有测试方法都会被执行、所有

@Before

@After

注解方法也会执行。

但是当你调用

statement.evaluate()

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

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

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

这个方法之所以要传入

notifier

,不是因为需要它来确定测试执行方案,而是用它来收集相关信息。我们当前是在 ParentRunner 类的

runner

实例中,执行测试所需信息都已经存在于这个实例中了,不需要任何额外信息。

classBlock

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

// org/junit/runners/ParentRunner.java// 第 212 行protectedStatementclassBlock(finalRunNotifier 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 行protectedfinalStatementwithInterruptIsolation(finalStatement statement){returnnewStatement(){@Overridepublicvoidevaluate()throwsThrowable{// 从这里进入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 行protectedStatementchildrenInvoker(finalRunNotifier notifier){returnnewStatement(){@Overridepublicvoidevaluate(){runChildren(notifier);// 就是这里}};}

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

// org/junit/runners/ParentRunner.java// 第 325 行privatevoidrunChildren(finalRunNotifier notifier){finalRunnerScheduler currentScheduler = scheduler;try{for(finalT each :getFilteredChildren()){// 遍历 child
            currentScheduler.schedule(newRunnable(){// 执行该 childpublicvoidrun(){ParentRunner.this.runChild(each, notifier);}});}}finally{
        currentScheduler.finished();}}

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

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

事实上,第二件事没有看起来那么复杂。这个

RunnerScheduler

接口的本意是加一层抽象,这样就能更灵活地支持除了顺序执行以外的其他调度方式——比如并行执行。

RunnerScheduler

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

也就是说,这个

currentScheduler

所做的事情其实就是立刻执行而已。。甚至连

finally{}

currentScheduler.finished();

方法也是个空方法。

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

privatevoidrunChildren(finalRunNotifier notifier){for(finalT each :getFilteredChildren()){// 遍历 childParentRunner.this.runChild(each, notifier);// 执行该 child}}

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

阶段 4:BlockJUnit4ClassRunner.runChild()

上面的代码自然把我们带到了

runChild()

方法里:

// org/junit/runners/BlockJUnit4ClassRunner.java// 第 91 行@OverrideprotectedvoidrunChild(finalFrameworkMethod method,RunNotifier notifier){Description description =describeChild(method);if(isIgnored(method)){
        notifier.fireTestIgnored(description);}else{Statement statement =newStatement(){@Overridepublicvoidevaluate()throwsThrowable{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 行protectedStatementmethodBlock(finalFrameworkMethod method){Object test;try{
        test =newReflectiveCallable(){@OverrideprotectedObjectrunReflectiveCall()throwsThrowable{returncreateTest(method);}}.run();}catch(Throwable e){returnnewFail(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 =newReflectiveCallable(){@OverrideprotectedObjectrunReflectiveCall()throwsThrowable{returncreateTest(method);}}.run();}catch(Throwable e){returnnewFail(e);}

这一部分的目的是构造一个测试类的实例。为什么呢?因为每个

@Test

测试都是一个方法,对吧?方法不能凭空执行啊(又不是 static 方法),所以需要在某个具体实例上执行。

比如,我这里执行的是

MyTest.simpleTest()

方法,所以实际上 JUnit 会先创建出来一个

MyTest

的实例

test

,然后在这个实例上执行该方法,即

test.simpleTest()

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

回到我们的代码,其中

ReflectiveCallable

的部分可能看起来有点儿晕,但那其实只是为了解决抛出

InvocationTargetException

异常时的小烦恼。如果忽略掉这一层面的逻辑,上述代码就可以简化成下面这样了:

Object test;try{
    test =createTest(method);}catch(Throwable e){returnnewFail(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 行protectedStatementmethodInvoker(FrameworkMethod method,Object test){returnnewInvokeMethod(method, test);}
methodInvoker

返回的是一个

Statement

,最终会被执行

evaluate()

,也就来到了下一阶段。

阶段 5:InvokeMethod.evaluate()

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

// org/junit/internal/runners/statements/InvokeMethod.javapublicclassInvokeMethodextendsStatement{privatefinalFrameworkMethod testMethod;privatefinalObject target;publicInvokeMethod(FrameworkMethod testMethod,Object target){this.testMethod = testMethod;this.target = target;}@Overridepublicvoidevaluate()throwsThrowable{
        testMethod.invokeExplosively(target);// 就是这里!}}

不知道为什么要起这么一个奇怪的名字

invokeExplosively

。。但总之,这里就是要执行我们的

simpleTest()

测试啦!来看下

invokeExplosively

的代码:

// org/junit/runners/model/FrameworkMethod.javapublicObjectinvokeExplosively(finalObject target,finalObject... params)throwsThrowable{returnnewReflectiveCallable(){@OverrideprotectedObjectrunReflectiveCall()throwsThrowable{return method.invoke(target, params);}}.run();}

前面提到过,

ReflectiveCallable

只是为了解决抛出

InvocationTargetException

异常时的小烦恼。如果忽略掉这一层面的逻辑,上述代码就可以简化成下面这样了:

publicObjectinvokeExplosively(finalObject target,finalObject... params)throwsThrowable{return method.invoke(target, params);}

其实就是执行我们的

simpleTest()

测试方法而已!其中:

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

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

小结

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

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

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

其次,组合模式的使用(也就是那个

Statement

接口)很好地封装了复杂性。

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

标签: 单元测试 java JUnit

本文转载自: https://blog.csdn.net/VoisSurTonChemin/article/details/125215728
版权归原作者 倪琛 所有, 如有侵权,请联系我们删除。

“JUnit 实现原理(1):测试是如何跑起来的”的评论:

还没有评论