浅解 JUnit 4 第九篇:JUnitCore (下)

73 阅读6分钟

背景

我在 浅解 JUnit 4 这个专栏陆续添加了一些文章。现有的文章初步探讨了 Test/Runner/RunnerBuilder 这些核心类的逻辑。但是 JUnit 4 的入口在哪里呢?本文会继续对这个问题进行探讨(由于 JUnitCore 这个类涉及的内容较多,一篇文章写不完,本文是 下篇,上篇的链接是 浅解 JUnit 4 第八篇:JUnitCore (上))。

要点

JUnitCore 类中的 runClasses(Class<?>...) 方法也是一个入口。这个入口涉及 JUnitCore 类中的 6 个方法 ⬇️ (下图中展示了这 6 个方法的代码)

image.png

为了便于讨论,我们给这些方法加上编号 ⬇️

  1. method1method_1: runClasses(Class<?>... classes)
  2. method2method_2: runClasses(Computer computer, Class<?>... classes)
  3. method3method_3: run(Computer computer, Class<?>... classes)
  4. method4method_4: run(Request request)
  5. method5method_5: run(Runner runner)
  6. method6method_6: defaultComputer()

本文的重点是 method3method_3: run(Computer computer, Class<?>... classes)

一些类的全限定类名

文中提到 JUnit 4 中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.runners.Suite 写成 Suite)。我在这一小节把简略类名和全限定类名的对应关系列出来

简略的类名全限定类名(Fully Qualified Class Name)
AllDefaultPossibilitiesBuilderorg.junit.internal.builders.AllDefaultPossibilitiesBuilder
Computerorg.junit.runner.Computer
Computer$1org.junit.runner.Computer$1
Computer$2org.junit.runner.Computer$2
JUnitCoreorg.junit.runner.JUnitCore
Requestorg.junit.runner.Request
Runnerorg.junit.runner.Runner
RunnerBuilderorg.junit.runners.model.RunnerBuilder
Suiteorg.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<?>...) 方法

根据 JUnitCorejavadoc 的描述(如下图所示 ⬇️),我们可以调用 runClasses(Class<?>...) 方法来运行测试。

image.png

这里涉及 6 个方法 ⬇️

  1. runClasses(Class<?>... classes)
  2. runClasses(Computer computer, Class<?>... classes)
  3. run(Computer computer, Class<?>... classes)
  4. run(Request request)
  5. run(Runner runner)
  6. defaultComputer()

6 个方法之间的调用关系如下图所示 ⬇️

image.png

我们逐个来看这 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) 方法所做的事情如下方的思维导图所示 ⬇️

image.png

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) 方法所做的事情如下方的思维导图所示 ⬇️

image.png

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) 方法所做的事情如下方的思维导图所示 ⬇️

image.png

我们在 Request 类的 classes(Computer computer, Class<?>... classes) 方法里打一个断点,断点的位置如下图所示 ⬇️

image.png

然后 debug SimpleAdderTest 里的 main 方法

image.png

可以观察到

  • builder 变量是 AllDefaultPossibilitiesBuilder 的一个实例
  • classes 变量是一个数组(其中只有一个元素: org.study.SimpleAdderTest.class)

我们在 Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法中打一个断点(位置如下图所示),让程序运行到断点这里

image.png

Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法,会创建 Suite 类的某个匿名子类的实例,因此也会调用 Suite 类中的某个构造函数,我们在这个构造函数里打一个断点(位置如下图所示),让程序运行到断点这里

image.png

Suite 类的这个构造函数中,可以观察到

  • builder 变量是 RunnerBuilder 的一个匿名子类 Computer$1 的实例
  • classes 变量是一个数组(其中只有一个元素: org.study.SimpleAdderTest.class)

image.png

通过执行以下命令,可以看到 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) 为何可以访问宿主类的成员 (第一部分) 一文

image.png

我们让程序继续运行,直到程序运行到下图 78 行所示的位置 image.png

可以观察到 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$2Suite 的子类(所以也是 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";
  }
}
image.png

基于以上观察,我们可以概括出 Request 类的 classes(Computer computer, Class<?>... classes) 方法所做的事情 ⬇️

image.png

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) 方法所做的事情如下方的思维导图所示 ⬇️

image.png

这个方法的 request 参数是通过 Request 类中 runner(final Runner runner) 方法构造的,所以我们可以简单将 Request 视为 Runner 的包装 ⬇️ image.png

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

参考资料