背景
我在 浅解 JUnit 4 这个专栏陆续添加了一些文章。现有的文章初步探讨了 Test/Runner/RunnerBuilder 这些核心类的逻辑。但是 JUnit 4 的入口在哪里呢?本文会继续对这个问题进行探讨(由于 JUnitCore 这个类涉及的内容较多,一篇文章写不完,本文是 下篇,上篇的链接是 浅解 JUnit 4 第八篇:JUnitCore (上))。
要点
JUnitCore 类中的 runClasses(Class<?>...) 方法也是一个入口。这个入口涉及 JUnitCore 类中的 6 个方法 ⬇️ (下图中展示了这 6 个方法的代码)
为了便于讨论,我们给这些方法加上编号 ⬇️
- :
runClasses(Class<?>... classes) - :
runClasses(Computer computer, Class<?>... classes) - :
run(Computer computer, Class<?>... classes) - :
run(Request request) - :
run(Runner runner) - :
defaultComputer()
本文的重点是 : run(Computer computer, Class<?>... classes)
一些类的全限定类名
文中提到 JUnit 4 中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.runners.Suite 写成 Suite)。我在这一小节把简略类名和全限定类名的对应关系列出来
| 简略的类名 | 全限定类名(Fully Qualified Class Name) |
|---|---|
AllDefaultPossibilitiesBuilder | org.junit.internal.builders.AllDefaultPossibilitiesBuilder |
Computer | org.junit.runner.Computer |
Computer$1 | org.junit.runner.Computer$1 |
Computer$2 | org.junit.runner.Computer$2 |
JUnitCore | org.junit.runner.JUnitCore |
Request | org.junit.runner.Request |
Runner | org.junit.runner.Runner |
RunnerBuilder | org.junit.runners.model.RunnerBuilder |
Suite | org.junit.runners.Suite |
正文
我创建了一个小项目来讨论本文的问题
项目结构
这个项目的结构和 浅解 JUnit 4 第七篇:AllDefaultPossibilitiesBuilder 一文中的 项目结构 那一小节所描述的相同。为了避免在两篇文章之间跳来跳去,我还是把项目结构在本文中复述一遍 ⬇️
这个项目中包含以下目录/文件
src/main/java/org/example目录下有以下文件SimpleAdder.java
src/test/java/org/study目录下有以下文件SimpleAdderTest.java
pom.xml(在项目顶层)
其中 SimpleAdder.java 的内容如下 ⬇️
package org.example;
public class SimpleAdder {
public int add(int a, int b) {
return a + b;
}
}
SimpleAdderTest.java 的内容如下 ⬇️
package org.study;
import org.example.SimpleAdder;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.JUnitCore;
public class SimpleAdderTest {
private final SimpleAdder simpleAdder = new SimpleAdder();
@Test
public void testAdd() {
Assert.assertEquals(2, simpleAdder.add(1, 1));
}
public static void main(String[] args) {
JUnitCore.runClasses(SimpleAdderTest.class);
}
}
pom.xml 的内容如下 ⬇️
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>junit-study</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
JUnitCore 里的 runClasses(Class<?>...) 方法
根据 JUnitCore 的 javadoc 的描述(如下图所示 ⬇️),我们可以调用 runClasses(Class<?>...) 方法来运行测试。
这里涉及 6 个方法 ⬇️
runClasses(Class<?>... classes)runClasses(Computer computer, Class<?>... classes)run(Computer computer, Class<?>... classes)run(Request request)run(Runner runner)defaultComputer()
这 6 个方法之间的调用关系如下图所示 ⬇️
我们逐个来看这 6 个方法 (第 3 个方法是本文的重点)
第 1 个方法: runClasses(Class<?>... classes)
这个方法的代码如下 ⬇️
/**
* Run the tests contained in <code>classes</code>. Write feedback while the tests
* are running and write stack traces for all failed tests after all tests complete. This is
* similar to {@link #main(String[])}, but intended to be used programmatically.
*
* @param classes Classes in which to find tests
* @return a {@link Result} describing the details of the test run and the failed tests.
*/
public static Result runClasses(Class<?>... classes) {
return runClasses(defaultComputer(), classes);
}
runClasses(Class<?>... classes) 方法所做的事情如下方的思维导图所示 ⬇️
第 2 个方法: runClasses(Computer computer, Class<?>... classes)
这个方法的代码如下 ⬇️
/**
* Run the tests contained in <code>classes</code>. Write feedback while the tests
* are running and write stack traces for all failed tests after all tests complete. This is
* similar to {@link #main(String[])}, but intended to be used programmatically.
*
* @param computer Helps construct Runners from classes
* @param classes Classes in which to find tests
* @return a {@link Result} describing the details of the test run and the failed tests.
*/
public static Result runClasses(Computer computer, Class<?>... classes) {
return new JUnitCore().run(computer, classes);
}
runClasses(Computer computer, Class<?>... classes) 方法所做的事情如下方的思维导图所示 ⬇️
第 3 个方法: run(Computer computer, Class<?>... classes)
这个方法的代码如下 ⬇️
/**
* Run all the tests in <code>classes</code>.
*
* @param computer Helps construct Runners from classes
* @param classes the classes containing tests
* @return a {@link Result} describing the details of the test run and the failed tests.
*/
public Result run(Computer computer, Class<?>... classes) {
return run(Request.classes(computer, classes));
}
run(Computer computer, Class<?>... classes) 方法所做的事情如下方的思维导图所示 ⬇️
我们在 Request 类的 classes(Computer computer, Class<?>... classes) 方法里打一个断点,断点的位置如下图所示 ⬇️
然后 debug SimpleAdderTest 里的 main 方法
可以观察到
builder变量是AllDefaultPossibilitiesBuilder的一个实例classes变量是一个数组(其中只有一个元素:org.study.SimpleAdderTest.class)
我们在 Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法中打一个断点(位置如下图所示),让程序运行到断点这里
Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法,会创建 Suite 类的某个匿名子类的实例,因此也会调用 Suite 类中的某个构造函数,我们在这个构造函数里打一个断点(位置如下图所示),让程序运行到断点这里
在 Suite 类的这个构造函数中,可以观察到
builder变量是RunnerBuilder的一个匿名子类Computer$1的实例classes变量是一个数组(其中只有一个元素:org.study.SimpleAdderTest.class)
通过执行以下命令,可以看到 org.junit.runner.Computer$1 这个类的简要情况
javap -cp junit-4.13.2.jar -p 'org.junit.runner.Computer$1'
上述命令的执行结果如下 ⬇️ 由此可见,org.junit.runner.Computer$1 确实是 RunnerBuilder 的子类
Compiled from "Computer.java"
class org.junit.runner.Computer$1 extends org.junit.runners.model.RunnerBuilder {
final org.junit.runners.model.RunnerBuilder val$builder;
final org.junit.runner.Computer this$0;
org.junit.runner.Computer$1(org.junit.runner.Computer, org.junit.runners.model.RunnerBuilder);
public org.junit.runner.Runner runnerForClass(java.lang.Class<?>) throws java.lang.Throwable;
}
如果想查看 org.junit.runner.Computer$1 这个类的详细信息,可以执行如下的命令
javap -cp junit-4.13.2.jar -v -p 'org.junit.runner.Computer$1'
这个命令的输出比较长,这里就不展示了,在它所生成的输出的基础上,我们可以反编译出 org.junit.runner.Computer$1 的代码(有些内容在 java 代码中无法体现,我做了些变通的处理)。以下是我反编译的结果(不保证绝对准确,仅供参考)
// 注意:以下代码是我手动反编译的结果,不保证绝对准确,仅供参考
class org.junit.runner.Computer$1 extends org.junit.runners.model.RunnerBuilder {
final org.junit.runners.model.RunnerBuilder val$builder;
final org.junit.runner.Computer this$0;
Computer$1(org.junit.runner.Computer arg0, org.junit.runners.model.RunnerBuilder arg1) {
this.this$0 = arg0;
this.val$builder = arg1;
super();
}
public org.junit.runner.Runner runnerForClass(java.lang.Class<?> arg0) throws java.lang.Throwable {
return this.this$0.getRunner(this.val$builder, arg0);
}
}
至于这个匿名子类的内容为什么是这样的,可以参考 [Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第一部分) 一文
我们让程序继续运行,直到程序运行到下图 78 行所示的位置
可以观察到 suite 变量的类型是 Computer$2。
通过执行以下命令,可以看到 org.junit.runner.Computer$2 这个类的简要情况
javap -cp junit-4.13.2.jar -p 'org.junit.runner.Computer$2'
上述命令的执行结果如下 ⬇️ 由此可见,org.junit.runner.Computer$2 是 Suite 的子类(所以也是 Runner 的子类)
Compiled from "Computer.java"
class org.junit.runner.Computer$2 extends org.junit.runners.Suite {
final org.junit.runner.Computer this$0;
org.junit.runner.Computer$2(org.junit.runner.Computer, org.junit.runners.model.RunnerBuilder, java.lang.Class[]);
protected java.lang.String getName();
}
如果想查看 org.junit.runner.Computer$2 这个类的详细信息,可以执行如下的命令
javap -cp junit-4.13.2.jar -v -p 'org.junit.runner.Computer$2'
这个命令的输出比较长,这里就不展示了,在它所生成的输出的基础上,我们可以反编译出 org.junit.runner.Computer$2 的代码(有些内容在 java 代码中无法体现,我做了些变通的处理)。以下是我反编译的结果(不保证绝对准确,仅供参考)
// 注意:以下代码是我手动反编译的结果,不保证绝对准确,仅供参考
class org.junit.runner.Computer$2 extends org.junit.runners.Suite {
final org.junit.runner.Computer this$0;
Computer$2(org.junit.runner.Computer arg0, org.junit.runners.model.RunnerBuilder arg1, java.lang.Class[] arg2) {
this.this$0 = arg0;
super(arg1, arg2);
}
protected java.lang.String getName() {
return "classes";
}
}
基于以上观察,我们可以概括出 Request 类的 classes(Computer computer, Class<?>... classes) 方法所做的事情 ⬇️
第 4 个方法: run(Request request)
这个方法的代码如下 ⬇️
/**
* Run all the tests contained in <code>request</code>.
*
* @param request the request describing tests
* @return a {@link Result} describing the details of the test run and the failed tests.
*/
public Result run(Request request) {
return run(request.getRunner());
}
run(Request request) 方法所做的事情如下方的思维导图所示 ⬇️
这个方法的 request 参数是通过 Request 类中 runner(final Runner runner) 方法构造的,所以我们可以简单将 Request 视为 Runner 的包装 ⬇️
第 5 个方法: run(Runner runner)
这个方法的代码如下 ⬇️
/**
* 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;
}
这个方法负责运行 Runner,并通知相关的 RunListener。其中涉及的细节比较多,我们在后续的文章中再展开说。本文不讲这些细节。
第 6 个方法: defaultComputer()
这个方法的代码如下 ⬇️
static Computer defaultComputer() {
return new Computer();
}
defaultComputer() 这个方法会返回 Computer 的一个实例。
其他
画“这 6 个方法之间的调用关系”一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
title 这 <i>6</i> 个方法之间的调用关系
*[#Orange]:<i>method<sub>1</sub></i>:
<i>runClasses(Class<?>...)</i>;
**[#pink] 会调用 <i>method<sub>6</sub></i>
**[#pink] 会调用 <i>method<sub>2</sub></i>
*[#Orange]:<i>method<sub>2</sub></i>:
<i>runClasses(Computer, Class<?>...)</i>;
**[#pink] 会调用 <i>method<sub>3</sub></i>
*[#Orange]:<i>method<sub>3</sub></i>:
<i>run(Computer, Class<?>...)</i>;
**[#pink] 会调用 <i>method<sub>4</sub></i>
*[#Orange]:<i>method<sub>4</sub></i>:
<i>run(Request)</i>;
**[#pink] 会调用 <i>method<sub>5</sub></i>
*[#Orange]:<i>method<sub>5</sub></i>:
<i>run(Runner)</i>;
*[#Orange]:<i>method<sub>6</sub></i>:
<i>defaultComputer()</i>;
legend right
橙色节点表示讨论范围内涵盖的 <i>6</i> 个方法
粉色节点表示当前的这个 <i>method</i> 调用了哪些其他 <i>method</i>
endlegend
@endmindmap
画“JUnitCore 类中与 runClasses(Class<?>... classes) 方法有关的 6 个方法”一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>JUnitCore</i> 类中与 <i>runClasses(Class<?>... classes)</i> 方法有关的 <i>6</i> 个方法
class org.junit.runner.JUnitCore {
+{static} Result runClasses(Class<?>... classes)
+{static} Result runClasses(Computer computer, Class<?>... classes)
+Result run(Computer computer, Class<?>... classes)
+Result run(Request request)
+Result run(Runner runner)
~{static} Computer defaultComputer()
}
note right of org.junit.runner.JUnitCore::"runClasses(Class<?>... classes)"
<i>return runClasses(defaultComputer(), classes);</i>
end note
note right of org.junit.runner.JUnitCore::"runClasses(Computer computer, Class<?>... classes)"
<i>return new JUnitCore().run(computer, classes);</i>
end note
note left of org.junit.runner.JUnitCore::"run(Computer computer, Class<?>... classes)"
<i>return run(Request.classes(computer, classes));</i>
end note
note left of org.junit.runner.JUnitCore::"run(Request request)"
<i>return run(request.getRunner());</i>
end note
note right of org.junit.runner.JUnitCore::"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;
end note
note left of org.junit.runner.JUnitCore::defaultComputer()
<i>return new Computer();</i>
end note
@enduml
画“runClasses(Class<?>... classes) 方法做了什么”一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
title <i>runClasses(Class<?>... classes)</i> 方法做了什么
* <i>runClasses(Class<?>... classes)</i> 方法
**[#Orange] 第 <i>1</i> 步: 调用 <i>defaultComputer()</i> 方法
***:此方法只有一行代码 <U+2193>
<i>return new Computer();</i>;
**[#Orange] 第 <i>2</i> 步: 调用 <i>runClasses(Computer computer, Class<?>... classes)</i> 方法
@endmindmap
画"runClasses(Computer computer, Class<?>... classes) 方法做了什么" 一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
top to bottom direction
title <i>runClasses(Computer computer, Class<?>... classes)</i> 方法做了什么
* <i>runClasses(Computer computer, Class<?>... classes)</i> 方法
**[#Orange] 第 <i>1</i> 步: 创建 <i>JUnitCore</i> 的实例
**[#Orange]:第 <i>2</i> 步: 通过 <i>JUnitCore</i> 的实例,
调用 <i>run(Computer computer, Class<?>... classes)</i> 方法;
@endmindmap
画"run(Computer computer, Class<?>... classes) 方法做了什么"一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
top to bottom direction
title <i>run(Computer computer, Class<?>... classes)</i> 方法做了什么
* <i>run(Computer computer, Class<?>... classes)</i> 方法
**[#Orange]:第 <i>1</i> 步: 调用 <i>Request</i> 类里的
<i>classes(Computer, Class<?>...)</i> 方法;
**[#Orange] 第 <i>2</i> 步: 调用 <i>run(Request request)</i> 方法
@endmindmap
画 “org.junit.runner.Computer$1 的继承体系” 一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>org.junit.runner.Computer$1</i> 的继承体系
caption 注意: 图中只画出了本文关心的字段/方法\n
abstract class org.junit.runners.model.RunnerBuilder
class "org.junit.runner.Computer$1" {
~final RunnerBuilder val$builder
~final Computer this$0
~Computer$1(Computer, RunnerBuilder)
+Runner runnerForClass(Class<?>) throws Throwable
}
org.junit.runners.model.RunnerBuilder <|-- "org.junit.runner.Computer$1"
@enduml
画 “org.junit.runner.Computer$2 的继承体系” 一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>org.junit.runner.Computer$2</i> 的继承体系
caption 注意: 图中只画出了本文关心的类/字段/方法\n
abstract class org.junit.runner.Runner
abstract class org.junit.runners.ParentRunner<T>
class org.junit.runners.Suite
class "org.junit.runner.Computer$2" {
~final Computer this$0
~Computer$2(Computer, RunnerBuilder, Class[])
#String getName()
}
org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.Suite : extend ParentRunner<Runner>
org.junit.runners.Suite <|-- "org.junit.runner.Computer$2"
@enduml
画“ org.junit.runner.Request 类中的 classes(Computer, Class<?>...) 方法所做的事情”一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
title <i> org.junit.runner.Request</i> 类中的 <i>classes(Computer, Class<?>...)</i> 方法所做的事情
*[#Orange] 1. 创建一个 <i>AllDefaultPossibilitiesBuilder</i> 类的实例 <b><i>builder</i></b>
*[#Orange] 2. 调用 <i>Computer</i> 类中的 <i>getSuite(final RunnerBuilder, Class<?>[])</i> 方法
**[#pink]:该方法会返回 <i>Suite</i> 类的一个匿名子类的实例 <b><i>suite</i></b>
(这个匿名子类是 <i>org.junit.runner.Computer$2</i>, 我们不必关心它的具体名称);
**[#pink]:<b><i>suite</i></b> 通过 <b><i>builder</i></b> 为测试类构建对应的 <i>Runner</i>
对测试类 <i>X,Y,...</i> 而言,
<b><i>builder</i></b> 可以构建对应的 <i>Runner (Runner<sub>X</sub>, Runner<sub>Y</sub>, ...)</i>
这些 <i>Runner</i> 都是 <b><i>suite</i></b> 的子节点;
*[#Orange] 3. 将 <b><i>suite</i></b> 包装成 <i>Request</i> 对象
legend right
橙色节点展示了这个方法所做的 <i>3</i> 件事情
粉色节点解释了第 <i>2</i> 件事情的一些细节
endlegend
@endmindmap
画 "run(Request request) 方法做了什么" 一图所用到的代码
我用 PlantUML 画了那张图,所用到的代码如下 ⬇️
@startmindmap
'https://plantuml.com/mindmap-diagram
top to bottom direction
title <i>run(Request request)</i> 方法做了什么
* <i>run(Request request)</i> 方法
**[#Orange]:第 <i>1</i> 步: 调用 <i>Request</i> 类里的
<i>getRunner()</i> 方法;
**[#Orange] 第 <i>2</i> 步: 调用 <i>run(Runner runner)</i> 方法
@endmindmap