浅解 Junit 4 第七篇:AllDefaultPossibilitiesBuilder

背景

在以下两篇文章里,我们已经探讨了两种具体的 Runner (即 IgnoredClassRunnerSuite)是如何构建的。

但这只是两种具体的情况,那么 Runner 构建的一般情形是怎样的呢 ?本文会对此进行探讨。本文的主角是 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.junit.internal.builders.AllDefaultPossibilitiesBuilder \text{org.junit.internal.builders.AllDefaultPossibilitiesBuilder} </math>org.junit.internal.builders.AllDefaultPossibilitiesBuilder

要点

AllDefaultPossibilitiesBuilder 会依次用 5 个候选的 RunnerBuilder 来执行下述操作

  • 用这个 RunnerBuilder 来构建对应的 Runner
    • 如果构建结果不是 null,则返回它
    • 如果构建结果是 null,则继续尝试下一个 RunnerBuilder

5 个候选的 RunnerBuilder

  • IgnoredBuilder
  • AnnotatedBuilder
  • SuiteMethodBuilder/NullBuilder
  • JUnit3Builder
  • JUnit4Builder

一些类的全限定类名

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

RunnerBuilder 系列

简略的类名 全限定类名(Fully Qualified Class Name)
RunnerBuilder org.junit.runners.model.RunnerBuilder
AllDefaultPossibilitiesBuilder org.junit.internal.builders.AllDefaultPossibilitiesBuilder
IgnoredBuilder org.junit.internal.builders.IgnoredBuilder
AnnotatedBuilder org.junit.internal.builders.AnnotatedBuilder
SuiteMethodBuilder org.junit.internal.builders.SuiteMethodBuilder
NullBuilder org.junit.internal.builders.NullBuilder
JUnit3Builder org.junit.internal.builders.JUnit3Builder
JUnit4Builder org.junit.internal.builders.JUnit4Builder

Runner 系列

简略的类名 全限定类名(Fully Qualified Class Name)
Runner org.junit.runner.Runner
ParentRunner org.junit.runners.ParentRunner
IgnoredClassRunner org.junit.internal.builders.IgnoredClassRunner
Suite org.junit.runners.Suite
JUnit38ClassRunner org.junit.internal.runners.JUnit38ClassRunner
BlockJUnit4ClassRunner org.junit.runners.BlockJUnit4ClassRunner
JUnit4 org.junit.runners.JUnit4

正文

项目结构

我创建了一个小项目来讨论本文的问题,这个项目中包含以下目录/文件(.idea/ 目录是 Intellij IDEA 生成的,可以忽略它)

  • src/main/java/org/example 目录下有以下文件
    • SimpleAdder.java
  • src/test/java/org/study 目录下有以下文件
    • SimpleAdderTest.java
  • pom.xml (在项目顶层)

其中 SimpleAdder.java 的内容如下

java 复制代码
package org.example;

public class SimpleAdder {
    public int add(int a, int b) {
        return a + b;
    }
}

SimpleAdderTest.java 的内容如下

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 复制代码
<?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>

AllDefaultPossibilitiesBuilder 是如何构建 Runner 的?

我们在 AllDefaultPossibilitiesBuilder 类的 runnerForClass(Class<?> testClass) 方法里打一个断点,断点的位置如下图所示

然后 debug SimpleAdderTestmain 方法。等运行到断点时,会看到 testClass 参数的值为 org.study.SimpleAdderTest.class

我们看一下 AllDefaultPossibilitiesBuilder 类中的 runnerForClass(Class<?> testClass) 方法的代码 👇

java 复制代码
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        List<RunnerBuilder> builders = Arrays.asList(
                ignoredBuilder(),
                annotatedBuilder(),
                suiteMethodBuilder(),
                junit3Builder(),
                junit4Builder());

        for (RunnerBuilder each : builders) {
            Runner runner = each.safeRunnerForClass(testClass);
            if (runner != null) {
                return runner;
            }
        }
        return null;
    }

runnerForClass(Class<?> testClass) 方法的逻辑是

  1. 构建 List<RunnerBuilder> 类型的 buildersbuilders 里的 5 个元素分别是
    • IgnoredBuilder 的实例
    • AnnotatedBuilder 的实例
    • SuiteMethodBuilder 的实例或 NullBuilder 的实例
    • JUnit3Builder 的实例
    • JUnit4Builder 的实例
  2. 依次尝试用 builders 中的各个元素来构建 Runner
    • 如果构建结果不是 null,则返回它
    • 如果构建结果是 null,则继续下一轮尝试
  3. 如果 builders 中的所有元素构建的结果都是 null,则返回 null

我们分别看看 builders 里的 5 个元素是如何构建 Runner

第一个元素: IgnoredBuilder 的实例

IgnoredBuilder 的代码是这样的 👇

java 复制代码
package org.junit.internal.builders;

import org.junit.Ignore;
import org.junit.runner.Runner;
import org.junit.runners.model.RunnerBuilder;

public class IgnoredBuilder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) {
        if (testClass.getAnnotation(Ignore.class) != null) {
            return new IgnoredClassRunner(testClass);
        }
        return null;
    }
}

它的 runnerForClass(Class<?> testClass) 方法的逻辑是

  • 如果 testClass 上有 @Ignore 注解,则返回对应的 IgnoredClassRunner
  • 否则返回 null

更多细节可以参考 浅解 Junit 4 第五篇:IgnoredBuilder 和 RunnerBuilder 一文

第二个元素: AnnotatedBuilder 的实例

AnnotatedBuilder 的主要代码如下 👇 (packageimport 语句已略去)

java 复制代码
public class AnnotatedBuilder extends RunnerBuilder {
    private static final String CONSTRUCTOR_ERROR_FORMAT = "Custom runner class %s should have a public constructor with signature %s(Class testClass)";

    private final RunnerBuilder suiteBuilder;

    public AnnotatedBuilder(RunnerBuilder suiteBuilder) {
        this.suiteBuilder = suiteBuilder;
    }

    @Override
    public Runner runnerForClass(Class<?> testClass) throws Exception {
        for (Class<?> currentTestClass = testClass; currentTestClass != null;
             currentTestClass = getEnclosingClassForNonStaticMemberClass(currentTestClass)) {
            RunWith annotation = currentTestClass.getAnnotation(RunWith.class);
            if (annotation != null) {
                return buildRunner(annotation.value(), testClass);
            }
        }

        return null;
    }

    private Class<?> getEnclosingClassForNonStaticMemberClass(Class<?> currentTestClass) {
        if (currentTestClass.isMemberClass() && !Modifier.isStatic(currentTestClass.getModifiers())) {
            return currentTestClass.getEnclosingClass();
        } else {
            return null;
        }
    }

    public Runner buildRunner(Class<? extends Runner> runnerClass,
            Class<?> testClass) throws Exception {
        try {
            return runnerClass.getConstructor(Class.class).newInstance(testClass);
        } catch (NoSuchMethodException e) {
            try {
                return runnerClass.getConstructor(Class.class,
                        RunnerBuilder.class).newInstance(testClass, suiteBuilder);
            } catch (NoSuchMethodException e2) {
                String simpleName = runnerClass.getSimpleName();
                throw new InitializationError(String.format(
                        CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName));
            }
        }
    }
}

为什么会有 AnnotatedBuilder?

有些时候,用户需要指定 Runner(一个典型的例子是指定 Suite 作为 Runner,此时就需要 @RunWith(Suite.class) 这样的内容)。而 AnnotatedBuilder 可以解析测试类上的 @RunWith 注解。假设 @RunWith 注解的 value() 方法的返回值为 XXXRunner.class(XXXRunner 必须是某个 Runner),那么 AnnotatedBuilder 会尝试调用 XXXRunner.class 中的构造函数,从而构建 XXXRunner 的实例。

我们先整体理解一下 runnerForClass(Class<?> testClass) 方法,然后再仔细看它的具体逻辑。不严谨地说,这个方法做的事情可以概括为以下两步

  1. 获取当前测试类上的 @RunWith 注解,并获取指定的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Runner: XXXRunner \text{Runner: XXXRunner} </math>Runner: XXXRunner 的类型信息
  2. 如果第一步中找到了 @RunWith 注解,再调用 buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass) 方法生成指定的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Runner: XXXRunner \text{Runner: XXXRunner} </math>Runner: XXXRunner 的实例

然后我们再看看其中的细节 👇

这个 runnerForClass(Class<?> testClass) 方法中有一个 for 循环,在这个循环的每一轮里,会

  • 检查当前测试类上是否有 @RunWith 注解
    • 如果有,则调用 buildRunner(Class<? extends Runner>, Class<?>) 方法来构建对应的 Runner
  • 否则,获取当前测试类的 EnclosingClass(姑且称之为外围类或者宿主类吧),继续进行下一轮循环

这么说可能有点抽象,我来举个具体的例子吧。假设我们用程序模拟了一个简单的 ALU 的功能,现在需要对其逻辑运算功能以及算术运算功能分别进行测试。测试用的代码也许会是这个样子 👇

java 复制代码
package org.study;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
public class ALUSuite {

    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(ALUSuite.LogicGateSuite.class);
        System.out.println(result.getFailures());
    }

    @Suite.SuiteClasses({AndGateTest.class, OrGateTest.class, NotGateTest.class})
    class LogicGateSuite {
    }

    @Suite.SuiteClasses({HalfAdderTest.class, FullAdderTest.class})
    class ArithmeticSuite {

    }
}

此时 @RunWith 注解是在 ALUSuite 这个类上,但测试类是 ALUSuite.LogicGateSuite.class

runnerForClass(Class<?> testClass) 方法里的 for 循环会运行两轮 👇

第几轮循环 currentTestClass 是什么 currentTestClass 上有 @RunWith 注解吗?
1 ALUSuite.LogicGateSuite 没有
2 ALUSuite

如果我们使用 @RunWith 注解时,指定的 RunnerSuite(即,value() 的返回值是 Suite.class),那么更多的细节可以参考 浅解 Junit 4 第六篇:AnnotatedBuilder 和 RunnerBuilder 一文。

第三个元素: 一个 SuiteMethodBuilder 的实例或 NullBuilder 的实例

SuiteMethod 类的 javadoc 来看 👇 SuiteMethodBuilder 应该是用于支持 JUnit 3.8 风格的 suite() 静态方法的,我们现在只探索 JUnit 4 的内容,JUnit 3.8 的内容就跳过吧。

NullBuilder 的代码很少,我复制到下方了 👇 从代码可以看出, NullBuilder 构建的 Runner 总会是 null

java 复制代码
package org.junit.internal.builders;

import org.junit.runner.Runner;
import org.junit.runners.model.RunnerBuilder;

public class NullBuilder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> each) throws Throwable {
        return null;
    }
}

第四个元素: 一个 JUnit3Builder 的实例

JUnit3Builder 的代码不多,我复制到下方了 👇 从代码(以及类名、方法名)来看,它是为了兼容旧的单元测试(JUnit 4 之前的单元测试)。我们现在只探索 JUnit 4 的内容,所以 JUnit3Builder 的内容就跳过吧。

java 复制代码
package org.junit.internal.builders;

import org.junit.internal.runners.JUnit38ClassRunner;
import org.junit.runner.Runner;
import org.junit.runners.model.RunnerBuilder;

public class JUnit3Builder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        if (isPre4Test(testClass)) {
            return new JUnit38ClassRunner(testClass);
        }
        return null;
    }

    boolean isPre4Test(Class<?> testClass) {
        return junit.framework.TestCase.class.isAssignableFrom(testClass);
    }
}

第五个元素: 一个 JUnit4Builder 的实例

JUnit4Builder 的代码很少,我复制到下方了 👇

java 复制代码
package org.junit.internal.builders;

import org.junit.runner.Runner;
import org.junit.runners.JUnit4;
import org.junit.runners.model.RunnerBuilder;

public class JUnit4Builder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        return new JUnit4(testClass);
    }
}

JUnit4BuilderrunnerForClass(Class<?> testClass) 方法会返回一个 JUnit4 类的实例。

其他

"org.junit.runners.model.RunnerBuilder 和它的一些子类" 一图是如何绘制的?

我用了 PlantUML 来画这张图,具体的代码如下

puml 复制代码
@startuml
'https://plantuml.com/class-diagram

title <i>org.junit.runners.model.RunnerBuilder</i> 和它的一些子类

abstract class 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

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

"org.junit.runner.Runner 和它的一些子类" 一图是如何绘制的?

我用了 PlantUML 来画这张图,具体的代码如下

puml 复制代码
@startuml
'https://plantuml.com/class-diagram

title <i>org.junit.runner.Runner</i> 和它的一些子类

abstract class org.junit.runner.Runner
abstract class org.junit.runners.ParentRunner<T>
class org.junit.internal.builders.IgnoredClassRunner
class org.junit.runners.Suite
class org.junit.internal.runners.JUnit38ClassRunner
class org.junit.runners.BlockJUnit4ClassRunner
class org.junit.runners.JUnit4

org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runner.Runner <|-- org.junit.internal.builders.IgnoredClassRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.Suite : extend ParentRunner<Runner>
org.junit.runner.Runner <|-- org.junit.internal.runners.JUnit38ClassRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : extends ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.junit.runners.JUnit4

@enduml

"org.junit.runners.JUnit4 继承体系简图" 是如何绘制的?

我用了 PlantUML 来画这张图,具体的代码如下

puml 复制代码
@startuml
'https://plantuml.com/class-diagram

title <i>org.junit.runners.JUnit4</i> 继承体系简图
caption 注意: 图中只画了本文关心的类/接口/方法/字段

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

org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : extends ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.junit.runners.JUnit4


abstract class org.junit.runner.Runner {
   +{abstract} void run(RunNotifier notifier)
}

abstract class org.junit.runners.ParentRunner<T> {
    +void run(final RunNotifier notifier)
    #{abstract} List<T> getChildren()
}

class org.junit.runners.BlockJUnit4ClassRunner {
    #List<FrameworkMethod> getChildren()
}

@enduml

参考资料

相关推荐
匀泪1 小时前
云原生(TOMCAT实验)
java·云原生·tomcat
BigGGGuardian1 小时前
给 Spring Boot 接口加了幂等保护:Token 机制 + 结果缓存,一个注解搞定
java·开源
Kiyra1 小时前
深入浅出远程连接:Java 后端视角下的底层原理与实践
java·开发语言
一只鹿鹿鹿1 小时前
数据治理文档(word原件)
java·运维·spring boot·后端
beata1 小时前
Java基础-12:Java IO深度解析与避坑指南:从底层原理到BIO NIO AIO实战
java·后端
Hx_Ma161 小时前
测试题(五)
java·开发语言·后端
yy.y--1 小时前
Java文件读取实战:用FileInputStream显示源码
java·开发语言
我是大猴子1 小时前
异常的处理
java·开发语言
亓才孓1 小时前
【MyBatis Exception】省略动态SQL中的‘‘,会造成Runtime Exception
java·服务器·mybatis