目录
背景
继上一个系列初步研究了单元测试神器 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 源码。
版权归原作者 倪琛 所有, 如有侵权,请联系我们删除。