与普通的测试类 对应的 会是 类型,如果我们有定制化的需求,能否自己实现一个 并让 JUnit 4 框架运行它呢?本文会探讨这个问题。
要点
- 通过继承 ,我们可以实现自己的
- 给测试类 加上
@RunWith(XXXRunner.class)注解,就可以让 来运行 中的测试方法
一些类的全限定类名
文中提到 JUnit 4 框架中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.internal.builders.AllDefaultPossibilitiesBuilder 写成 AllDefaultPossibilitiesBuilder)。我在这一小节把简略类名和全限定类名的对应关系列出来
RunnerBuilder 系列
| 简略的类名 | 全限定类名(Fully Qualified Class Name) | 是什么 |
|---|---|---|
AllDefaultPossibilitiesBuilder | org.junit.internal.builders.AllDefaultPossibilitiesBuilder | 类 (class) |
IgnoredBuilder | org.junit.internal.builders.IgnoredBuilder | 类 (class) |
AnnotatedBuilder | org.junit.internal.builders.AnnotatedBuilder | 类 (class) |
SuiteMethodBuilder | org.junit.internal.builders.SuiteMethodBuilder | 类 (class) |
NullBuilder | org.junit.internal.builders.NullBuilder | 类 (class) |
JUnit3Builder | org.junit.internal.builders.JUnit3Builder | 类 (class) |
JUnit4Builder | org.junit.internal.builders.JUnit4Builder | 类 (class) |
RunnerBuilder | org.junit.runners.model.RunnerBuilder | 抽象类(abstract class) |
RunWith 或 @RunWith | org.junit.runner.RunWith | 注解(annotation) |
Runner 系列
| 简略的类名 | 全限定类名(Fully Qualified Class Name) | 是什么 |
|---|---|---|
Runner | org.junit.runner.Runner | 抽象类(abstract class) |
ParentRunner | org.junit.runners.ParentRunner | 抽象类(abstract class) |
BlockJUnit4ClassRunner | org.junit.runners.BlockJUnit4ClassRunner | 类 (class) |
JUnit4 | org.junit.runners.JUnit4 | 类 (class) |
MyRunner | org.study.runners.MyRunner | 类 (class) |
正文
对普通的测试类 而言,它对应的 会是 类型。它的简要类图如下 ⬇️
如果我们想统计每个测试方法的耗时,一个方案是实现一个专门的 来完成这个逻辑。由于 是 final class,我们无法继承它。但是我们可以继承它的父类,即 。下一小节会展示项目中的代码。
项目代码
项目结构如下
.
├── 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 的类图如下 ⬇️
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 的简要类图如下 ⬇️
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 的类图如下
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>
运行结果
运行 SimpleProductCalculatorTest 的 main 方法,应该可以看到类似下图的效果(具体的运行耗时可能有差异)
每个测试方法的执行耗时都被展示出来了。
原理简述
至于为什么在 SimpleProductCalculatorTest 类上添加 @RunWith(MyRunner.class) 注解之后,就能让 JUnit 4 框架使用我们指定的 (即 ),我们可以通过打断点来一探究竟。考虑到一定会生成 的实例,如果我们在 的构造函数里打断点,应该就能找到这里的关键了。断点的位置如下图所示
为了便于叙述,我们把这个断点称为 断点甲。我们 debug SimpleProductCalculatorTest 的 main 方法,当程序运行到 断点甲 时,可以看到函数调用栈里有如下的内容
点击它之后,会来到 里。
可以观察到 AnnotatedBuilder 里的 runnerForClass(Class<?> testClass) 方法调用了 buildRunner(Class<? extends Runner> runnerClass, Class<?> testClass) 方法(如下图所示)
在 断点甲 的函数调用栈里继续观察,会找到如下内容 ⬇️
这样就来到了 类的 runnerForClass(Class<?> testClass) 方法。AllDefaultPossibilitiesBuilder 会负责尝试为不同情况的测试类构建对应的 。其中的细节可以参考 浅解 JUnit 4 第七篇:AllDefaultPossibilitiesBuilder 一文。
原理可以用下图概括
其他
用 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