浅解 JUnit 4 第十七篇:如何实现一个简单的 Runner?

72 阅读5分钟

与普通的测试类 T\text{T} 对应的 Runner:RunnerT\text{Runner}:\text{Runner}_\text{T} 会是 org.junit.runners.JUnit4\text{org.junit.runners.JUnit4} 类型,如果我们有定制化的需求,能否自己实现一个 Runner\text{Runner} 并让 JUnit 4 框架运行它呢?本文会探讨这个问题。

要点

  • 通过继承 org.junit.runners.BlockJUnit4ClassRunner\text{org.junit.runners.BlockJUnit4ClassRunner},我们可以实现自己的 Runner\text{Runner}
  • 给测试类 T\text{T} 加上 @RunWith(XXXRunner.class) 注解,就可以让 XXXRunner\text{XXXRunner} 来运行 T\text{T} 中的测试方法

一些类的全限定类名

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

RunnerBuilder 系列

简略的类名全限定类名(Fully Qualified Class Name)是什么
AllDefaultPossibilitiesBuilderorg.junit.internal.builders.AllDefaultPossibilitiesBuilder类 (class)
IgnoredBuilderorg.junit.internal.builders.IgnoredBuilder类 (class)
AnnotatedBuilderorg.junit.internal.builders.AnnotatedBuilder类 (class)
SuiteMethodBuilderorg.junit.internal.builders.SuiteMethodBuilder类 (class)
NullBuilderorg.junit.internal.builders.NullBuilder类 (class)
JUnit3Builderorg.junit.internal.builders.JUnit3Builder类 (class)
JUnit4Builderorg.junit.internal.builders.JUnit4Builder类 (class)
RunnerBuilderorg.junit.runners.model.RunnerBuilder抽象类(abstract class)
RunWith@RunWithorg.junit.runner.RunWith注解(annotation)

image.png

Runner 系列

简略的类名全限定类名(Fully Qualified Class Name)是什么
Runnerorg.junit.runner.Runner抽象类(abstract class)
ParentRunnerorg.junit.runners.ParentRunner抽象类(abstract class)
BlockJUnit4ClassRunnerorg.junit.runners.BlockJUnit4ClassRunner类 (class)
JUnit4org.junit.runners.JUnit4类 (class)
MyRunnerorg.study.runners.MyRunner类 (class)
image.png

正文

对普通的测试类 T\text{T} 而言,它对应的 Runner\text{Runner} 会是 org.junit.runners.JUnit4\text{org.junit.runners.JUnit4} 类型。它的简要类图如下 ⬇️

image.png

如果我们想统计每个测试方法的耗时,一个方案是实现一个专门的 Runner\text{Runner} 来完成这个逻辑。由于 org.junit.runners.JUnit4\text{org.junit.runners.JUnit4}final class,我们无法继承它。但是我们可以继承它的父类,即 org.junit.runners.BlockJUnit4ClassRunner\text{org.junit.runners.BlockJUnit4ClassRunner}。下一小节会展示项目中的代码。

项目代码

项目结构如下

.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── org
    │           └── study
    │               └── SimpleProductCalculator.java
    └── test
        └── java
            └── org
                └── study
                    ├── runners
                    │   └── MyRunner.java
                    └── SimpleProductCalculatorTest.java

SimpleProductCalculator.java

SimpleProductCalculator.java 的代码如下 ⬇️ (SimpleProductCalculator 用原始的方式计算两个非负整数的乘积)

package org.study;

public class SimpleProductCalculator {
    /**
     * Calculate the product of two non-negative integer in a naive way.
     * The result may overflow whe the actual product is big enough.
     *
     * @param a a non-negative integer
     * @param b a non-negative integer
     * @return the product of a, b
     */
    public int calculateProduct(int a, int b) {
        int product = 0;
        while (b > 0) {
            product += a;
            b--;
        }
        return product;
    }
}

SimpleProductCalculator 的类图如下 ⬇️

image.png

MyRunner.java

MyRunner.java 的代码如下

package org.study.runners;

import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

public class MyRunner extends BlockJUnit4ClassRunner {

    public MyRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }

    /**
     * Run child (i.e. a test method) can show time cost for it.
     */
    @Override
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        long t1 = System.currentTimeMillis();
        super.runChild(method, notifier);
        long t2 = System.currentTimeMillis();

        String message = String.format(
                "It took %s ms to run [%s] test method",
                (t2 - t1), method.getName()
        );
        System.out.println(message);
    }
}

MyRunner 的简要类图如下 ⬇️

image.png

SimpleProductCalculatorTest.java

SimpleProductCalculatorTest.java 的代码如下 ⬇️

package org.study;

import org.junit.*;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runner.notification.Failure;
import org.study.runners.MyRunner;

@RunWith(MyRunner.class)
public class SimpleProductCalculatorTest {

    private final SimpleProductCalculator productCalculator = new SimpleProductCalculator();

    @Test
    public void test_case1() {
        int a = 1000;
        int b = 1000;
        Assert.assertEquals(a * b, productCalculator.calculateProduct(a, b));
    }

    @Test
    public void test_case2() {
        int a = 10000;
        int b = 10000;
        Assert.assertEquals(a * b, productCalculator.calculateProduct(a, b));
    }

    @Test
    public void test_case3() {
        int a = 10;
        int b = 1_0000_0000;
        Assert.assertEquals(a * b, productCalculator.calculateProduct(a, b));
    }

    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(SimpleProductCalculatorTest.class);
        for (Failure failure : result.getFailures()) {
            System.out.println(failure);
        }
    }
}

SimpleProductCalculatorTest 的类图如下

image.png

pom.xml

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>25</maven.compiler.source>
        <maven.compiler.target>25</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>

运行结果

运行 SimpleProductCalculatorTestmain 方法,应该可以看到类似下图的效果(具体的运行耗时可能有差异)

image.png

每个测试方法的执行耗时都被展示出来了。

原理简述

至于为什么在 SimpleProductCalculatorTest 类上添加 @RunWith(MyRunner.class) 注解之后,就能让 JUnit 4 框架使用我们指定的 Runner\text{Runner}(即 MyRunner\text{MyRunner}),我们可以通过打断点来一探究竟。考虑到一定会生成 MyRunner\text{MyRunner} 的实例,如果我们在 MyRunner\text{MyRunner} 的构造函数里打断点,应该就能找到这里的关键了。断点的位置如下图所示

image.png

为了便于叙述,我们把这个断点称为 断点甲。我们 debug SimpleProductCalculatorTestmain 方法,当程序运行到 断点甲 时,可以看到函数调用栈里有如下的内容

image.png

点击它之后,会来到 org.junit.internal.builders.AnnotatedBuilder\text{org.junit.internal.builders.AnnotatedBuilder} 里。 可以观察到 AnnotatedBuilder 里的 runnerForClass(Class<?> testClass) 方法调用了 buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass) 方法(如下图所示)

image.png

断点甲 的函数调用栈里继续观察,会找到如下内容 ⬇️

image.png

这样就来到了 org.junit.internal.builders.AllDefaultPossibilitiesBuilder\text{org.junit.internal.builders.AllDefaultPossibilitiesBuilder} 类的 runnerForClass(Class<?> testClass) 方法。AllDefaultPossibilitiesBuilder 会负责尝试为不同情况的测试类构建对应的 Runner\text{Runner}。其中的细节可以参考 浅解 JUnit 4 第七篇:AllDefaultPossibilitiesBuilder 一文。

原理可以用下图概括

image.png

其他

用 PlantUML 画图,所用到的代码列举如下

画 "一些 RunnerBuilder" 所用到的代码

@startuml

title 一些 <i>RunnerBuilder</i>

abstract org.junit.runners.model.RunnerBuilder
class org.junit.internal.builders.AllDefaultPossibilitiesBuilder
class org.junit.internal.builders.IgnoredBuilder
class org.junit.internal.builders.AnnotatedBuilder
class org.junit.internal.builders.SuiteMethodBuilder
class org.junit.internal.builders.NullBuilder
class org.junit.internal.builders.JUnit3Builder
class org.junit.internal.builders.JUnit4Builder
annotation org.junit.runner.RunWith


org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.AllDefaultPossibilitiesBuilder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.IgnoredBuilder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.AnnotatedBuilder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.SuiteMethodBuilder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.NullBuilder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.JUnit3Builder
org.junit.runners.model.RunnerBuilder <|-- org.junit.internal.builders.JUnit4Builder


@enduml

画 "一些 Runner" 所用到的代码

@startuml

title 一些 <i>Runner</i>

abstract org.junit.runner.Runner
abstract org.junit.runners.ParentRunner<T>
class org.junit.runners.BlockJUnit4ClassRunner
class org.junit.runners.JUnit4
class org.study.runners.MyRunner

org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.junit.runners.JUnit4
org.junit.runners.BlockJUnit4ClassRunner <|-- org.study.runners.MyRunner
@enduml

画 "org.junit.runners.JUnit4 的简要类图" 所用到的代码

@startuml

title <i>org.junit.runners.JUnit4</i> 的简要类图

interface org.junit.runner.Describable
abstract org.junit.runner.Runner
interface org.junit.runner.manipulation.Filterable
interface org.junit.runner.manipulation.Sortable
interface org.junit.runner.manipulation.Orderable
abstract org.junit.runners.ParentRunner<T>
class org.junit.runners.BlockJUnit4ClassRunner
class org.junit.runners.JUnit4

org.junit.runner.Describable <|.. org.junit.runner.Runner
org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runner.manipulation.Filterable <|.. org.junit.runners.ParentRunner
org.junit.runner.manipulation.Sortable <|-- org.junit.runner.manipulation.Orderable
org.junit.runner.manipulation.Orderable <|.. org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : extends ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.junit.runners.JUnit4

@enduml

画 "org.study.SimpleProductCalculator 的类图" 所用到的代码

@startuml

title <i>org.study.SimpleProductCalculator</i> 的类图

class org.study.SimpleProductCalculator {
    + int calculateProduct(int a, int b)
}

note top of org.study.SimpleProductCalculator
<i>SimpleProductCalculator</i> 会用原始的方式来计算两个非负整数的乘积
(这里不考虑溢出问题)
end note

@enduml

画 "org.study.runners.MyRunner 的简要类图" 所用到的代码

@startuml

title <i>org.study.runners.MyRunner</i> 的简要类图
caption \n\n
' caption 中的内容是为了防止掘金平台自动生成的水印遮盖图中的文字

interface org.junit.runner.Describable
abstract org.junit.runner.Runner
interface org.junit.runner.manipulation.Filterable
interface org.junit.runner.manipulation.Sortable
interface org.junit.runner.manipulation.Orderable
abstract org.junit.runners.ParentRunner<T>
class org.junit.runners.BlockJUnit4ClassRunner
class org.junit.runners.JUnit4
class org.study.runners.MyRunner

org.junit.runner.Describable <|.. org.junit.runner.Runner
org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runner.manipulation.Filterable <|.. org.junit.runners.ParentRunner
org.junit.runner.manipulation.Sortable <|-- org.junit.runner.manipulation.Orderable
org.junit.runner.manipulation.Orderable <|.. org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : extends ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.junit.runners.JUnit4
org.junit.runners.BlockJUnit4ClassRunner <|-- org.study.runners.MyRunner

class org.study.runners.MyRunner {
    + MyRunner(Class<?>) throws InitializationError
    # void runChild(FrameworkMethod, RunNotifier)
}

note bottom of org.study.runners.MyRunner
<i>MyRunner</i> 中的 <i>runChild(FrameworkMethod, RunNotifier)</i> 方法
会运行对应的子节点(即, 对应的测试方法), 并展示其耗时
end note

@enduml

画 "org.study.SimpleProductCalculatorTest 的类图" 所用到的代码

@startuml

title <i>org.study.SimpleProductCalculatorTest</i> 的类图
caption \n\n
' caption 中的内容只是为了防止掘金平台自动生成的水印遮盖图中的内容

class org.study.SimpleProductCalculatorTest {
    - final SimpleProductCalculator productCalculator
    + void test_case1()
    + void test_case2()
    + void test_case3()
    + {static} void main(String[] args)
}

note bottom of org.study.SimpleProductCalculatorTest
<i>SimpleProductCalculatorTest</i> 类上带有 <i>@RunWith</i> 注解
通过使用 <i>@RunWith</i> 注解,
我们指定用 <i>MyRunner</i> 来运行 <i>SimpleProductCalculatorTest</i> 中的测试
end note

@enduml

画 "原理简述" 所用到的代码

@startmindmap

top to bottom direction

title 原理简述

caption \n\n
' caption 中的内容只是为了防止掘金自动生成的水印遮盖图中的文字

*:<i>JUnit 4</i> 框架会使用 <i>AllDefaultPossibilitiesBuilder</i>
为 <i>SimpleProductCalculatorTest</i> 构建对应的 <i>Runner: Runner<sub>SimpleProductCalculatorTest</sub></i>;
**:<i>AllDefaultPossibilitiesBuilder</i> 会依次尝试
使用 <i>5</i> 个 <i>RunnerBuilder</i> 来构建 <i>Runner</i>;
***_:<i>5</i> 个 <b><i>RunnerBuilder</i></b> <:point_down:>
1. <i>IgnoredBuilder</i> (处理类上带有 <i>@Ignore</i> 注解的情况)
2. <b><i>AnnotatedBuilder</i></b> (处理类上带有 <b><i>@RunWith</i></b> 注解的情况)
3. <i>SuiteMethodBuilder/NullBuilder</i> (与本文无关, 略)
4. <i>JUnit3Builder</i> (处理测试类是 <i>junit.framework.TestCase</i> 的子类的情况)
5. <i>JUnit4Builder</i> (兜底);
****:由于 <i>SimpleProductCalculatorTest</i> 带有 <b><i>@RunWith</i></b> 注解,
所以第 <i>2</i> 个 <i>RunnerBuilder</i> (即 <b><i>AnnotatedBuilder</i></b>)
会负责 <i>Runner<sub>SimpleProductCalculatorTest</sub></i> 的构建工作;
*****:<b><i>AnnotatedBuilder</i></b> 会通过反射调用 <i>MyRunner</i> 的构造函数
从而创建 <i>MyRunner</i> 的实例
这个实例就是与 <i>SimpleProductCalculatorTest</i> 对应的 <i>Runner: Runner<sub>SimpleProductCalculatorTest</sub></i>;

@endmindmap

参考资料