浅解 Junit 4 第七篇:AllDefaultPossibilitiesBuilder

1 阅读7分钟

背景

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

但这只是两种具体的情况,那么 Runner 构建的一般情形是怎样的呢?本文会对此进行探讨。本文的主角是 org.junit.internal.builders.AllDefaultPossibilitiesBuilder\text{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)
RunnerBuilderorg.junit.runners.model.RunnerBuilder
AllDefaultPossibilitiesBuilderorg.junit.internal.builders.AllDefaultPossibilitiesBuilder
IgnoredBuilderorg.junit.internal.builders.IgnoredBuilder
AnnotatedBuilderorg.junit.internal.builders.AnnotatedBuilder
SuiteMethodBuilderorg.junit.internal.builders.SuiteMethodBuilder
NullBuilderorg.junit.internal.builders.NullBuilder
JUnit3Builderorg.junit.internal.builders.JUnit3Builder
JUnit4Builderorg.junit.internal.builders.JUnit4Builder

image.png

Runner 系列

简略的类名全限定类名(Fully Qualified Class Name)
Runnerorg.junit.runner.Runner
ParentRunnerorg.junit.runners.ParentRunner
IgnoredClassRunnerorg.junit.internal.builders.IgnoredClassRunner
Suiteorg.junit.runners.Suite
JUnit38ClassRunnerorg.junit.internal.runners.JUnit38ClassRunner
BlockJUnit4ClassRunnerorg.junit.runners.BlockJUnit4ClassRunner
JUnit4org.junit.runners.JUnit4

image.png

正文

项目结构

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

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

其中 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>

AllDefaultPossibilitiesBuilder 是如何构建 Runner 的?

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

image.png

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

image.png

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

    @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 的代码是这样的 👇

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 语句已略去)

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 注解,并获取指定的 Runner: XXXRunner\text{Runner: XXXRunner} 的类型信息
  2. 如果第一步中找到了 @RunWith 注解,再调用 buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass) 方法生成指定的 Runner: XXXRunner\text{Runner: XXXRunner} 的实例

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

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

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

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

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

image.png

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

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

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

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

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

image.png

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

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 的内容就跳过吧。

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 的代码很少,我复制到下方了 👇

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

image.png

其他

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

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

@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 来画这张图,具体的代码如下

@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 来画这张图,具体的代码如下

@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

参考资料