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

1 阅读6分钟

背景

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

要点

image.png

正文

我创建了一个小项目来讨论本文的问题

项目结构

这个项目的结构和 浅解 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()

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)

我们 Step Into Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法

image.png

Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法,会创建 Suite 类的某个匿名子类的实例,因此也会调用 Suite 类中的某个构造函数,我们 Step Into 这个构造函数

image.png

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

  • builder 变量是 RunnerBuilder 的一个匿名子类的实例(它可以访问一个 AllDefaultPossibilitiesBuilder 的实例)
  • classes 变量是一个数组(其中只有一个元素: org.study.SimpleAdderTest.class)

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

  1. 创建一个 AllDefaultPossibilitiesBuilder 类的实例 builder\text{builder}
  2. 调用 Computer 类中的 getSuite(final RunnerBuilder builder, Class<?>[] classes) 方法
    • 该方法会返回 Suite 类的某个匿名子类的实例 suite\text{suite}
    • suite\text{suite} 可以(间接地)通过 builder\text{builder} 来为测试类构建对应的 Runner
    • 对测试类 T1,T2,\text{T}_1,\text{T}_2,\cdots 而言,builder\text{builder} 可以构建对应的 Runner: RunnerT1,RunnerT2,\text{Runner}_{\text{T}_1},\text{Runner}_{\text{T}_2},\cdots
    • RunnerT1,RunnerT2,\text{Runner}_{\text{T}_1},\text{Runner}_{\text{T}_2},\cdots 都是 suite\text{suite} 的子节点
  3. suite\text{suite} 包装成 Request 对象

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 的一个实例。

其他

画“JUnitCore 类的一个入口: runClasses(Class<?>... classes) 方法”一图所用到的代码

我用 PlantUML 画了那张图,所用到的代码如下 ⬇️

@startuml
'https://plantuml.com/class-diagram

title <i>JUnitCore</i> 类的一个入口: <i>runClasses(Class<?>... classes)</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> 方法
***:此方法只有一行代码 ⬇️
<i>return new Computer();</i>;
**[#Orange] 第 <i>2</i> 步: 调用 <i>runClasses(Computer computer, Class<?>... classes)</i> 方法

caption 橙色节点展示了 <i>runClasses(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

画 "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

参考资料