背景
在以下两篇文章里,我们已经探讨了两种具体的 Runner (即 IgnoredClassRunner 和 Suite)是如何构建的。
但这只是两种具体的情况,那么 Runner 构建的一般情形是怎样的呢?本文会对此进行探讨。本文的主角是
要点
AllDefaultPossibilitiesBuilder 会依次用 5 个候选的 RunnerBuilder 来执行下述操作
- 用这个
RunnerBuilder来构建对应的Runner- 如果构建结果不是
null,则返回它 - 如果构建结果是
null,则继续尝试下一个RunnerBuilder
- 如果构建结果不是
5 个候选的 RunnerBuilder 是
IgnoredBuilderAnnotatedBuilderSuiteMethodBuilder/NullBuilderJUnit3BuilderJUnit4Builder
一些类的全限定类名
文中提到 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 的内容如下
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) 方法里打一个断点,断点的位置如下图所示
然后 debug SimpleAdderTest 的 main 方法。等运行到断点时,会看到 testClass 参数的值为 org.study.SimpleAdderTest.class
我们看一下 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) 方法的逻辑是
- 构建
List<RunnerBuilder>类型的builders,builders里的5个元素分别是IgnoredBuilder的实例AnnotatedBuilder的实例SuiteMethodBuilder的实例或NullBuilder的实例JUnit3Builder的实例JUnit4Builder的实例
- 依次尝试用
builders中的各个元素来构建Runner- 如果构建结果不是
null,则返回它 - 如果构建结果是
null,则继续下一轮尝试
- 如果构建结果不是
- 如果
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 的主要代码如下 👇 (package 和 import 语句已略去)
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) 方法,然后再仔细看它的具体逻辑。不严谨地说,这个方法做的事情可以概括为以下两步
- 获取当前测试类上的
@RunWith注解,并获取指定的 的类型信息- 如果第一步中找到了
@RunWith注解,再调用buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass)方法生成指定的 的实例
然后我们再看看其中的细节 👇
这个 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
runnerForClass(Class<?> testClass) 方法里的 for 循环会运行两轮 👇
| 第几轮循环 | currentTestClass 是什么 | currentTestClass 上有 @RunWith 注解吗? |
|---|---|---|
1 | ALUSuite.LogicGateSuite | 没有 |
2 | ALUSuite | 有 |
如果我们使用 @RunWith 注解时,指定的 Runner 是 Suite(即,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。
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);
}
}
JUnit4Builder 的 runnerForClass(Class<?> testClass) 方法会返回一个 JUnit4 类的实例。
其他
“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