背景
在 浅解 JUnit 4 第四篇:类上的 @Ignore 注解 一文中,我们初步探讨了 类上的 @Ignore 注解 是如何起作用的。那么 方法上的 @Ignore 注解 又是如何起作用的呢?本文会对这个问题进行探讨。
要点
JUnit 4会为测试类生成对应的Runner- 对一个测试类 而言,典型的情况为 ⬇️
JUnit 4为其生成的Runner: 是org.junit.runners.JUnit4的实例
ParentRunner会通过runChildren(RunNotifier)方法来查找所有子节点并运行对应的测试runChildren(RunNotifier)方法会调用runChild(T, RunNotifier)方法BlockJUnit4ClassRunner中override了runChild(FrameworkMethod, RunNotifier)方法BlockJUnit4ClassRunner中的runChild(FrameworkMethod, RunNotifier)方法里会检查当前方法节点是否带有@Ignore注解,如果有,则略过这个方法
我把重要的类/方法在下图中展示出来了 ⬇️
一些类的全限定类名
文中提到 JUnit 4 中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.runners.Suite 写成 Suite)。我在这一小节把简略类名和全限定类名的对应关系列出来
| 简略的类名 | 全限定类名(Fully Qualified Class Name) |
|---|---|
BlockJUnit4ClassRunner | org.junit.runners.BlockJUnit4ClassRunner |
Computer$2 | org.junit.runner.Computer$2 |
FrameworkMethod | org.junit.runners.model.FrameworkMethod |
JUnit4 | org.junit.runners.JUnit4 |
ParentRunner | org.junit.runners.ParentRunner |
Runner | org.junit.runner.Runner |
Suite | org.junit.runners.Suite |
TestClass | org.junit.runners.model.TestClass |
正文
一个具体的例子
我在本地创建了一个小项目来以便探讨本文的问题,这个项目的结构如下 ⬇️
├── pom.xml
└── src
└── test
└── java
└── org
└── study
└── SimpleTest.java
其中 SimpleTest.java 文件的内容如下 ⬇️
package org.study;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.JUnitCore;
public class SimpleTest {
@Test
public void test1() {
Assert.assertEquals(2, 1 + 1);
}
@Test
public void test2() {
Assert.assertEquals(1, 2 - 1);
}
@Test
@Ignore
public void test3() {
Assert.assertEquals(81, 9 * 9);
}
public static void main(String[] args) {
JUnitCore.runClasses(SimpleTest.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>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>
分析
我们在 ParentRunner 类的 runChildren(final RunNotifier notifier) 方法里打一个断点(断点的位置如下图所示)
然后 debug SimpleTest 的 main 方法。当程序运行到断点这里时,可以观察到 this 的类型是 Computer$2
虽然我们只写了 SimpleTest 一个类,但是 JUnit 4 会为我们生成很多相关的对象,其中的一部分对象如下图所示 ⬇️
第一次遇到断点
当程序第一次运行到 328 行的断点那里时,可以观察到 getFilteredChildren() 方法所返回的 List 中只包含一个元素,而这个元素就是 SimpleTest 对应的 Runner ⬇️
第二次遇到断点
然后我们让程序继续运行,当程序第二次运行到 328 行的断点那里时,可以观察到 getFilteredChildren() 方法所返回的 List 中包含 3 个元素,而这 3 个元素就是 SimpleTest 中的那三个带有 @Test 注解的方法对应的 FrameworkMethod 对象(可以简单将 FrameworkMethod 理解为让 方法(method) 更容易使用的一个辅助类)
既然调用 上的 getFilteredChildren() 方法时,返回值中有 3 个元素,那么 test3() 这个方法为什么没有运行呢?
带有 @Ignore 注解的方法是什么时候被剔除的?
我们在 ParentRunner.java 的第 331 行再新增一个断点(断点的位置如下图所示)
让程序继续运行到新增的断点这里。然后 Step Into 这个方法,我们会来到 BlockJUnit4ClassRunner 类的 runChild(final FrameworkMethod method, RunNotifier notifier) 方法里。可以观察到 this 的类型是 JUnit4(这个类的全限定类名是 org.junit.runners.JUnit4),而 BlockJUnit4ClassRunner.java 的第 94 行看起来有点可疑 ⬇️
我们去看一下第 94 行调用的 isIgnored(FrameworkMethod child) 方法。我把这个方法的代码复制到下方了
/**
* Evaluates whether {@link FrameworkMethod}s are ignored based on the
* {@link Ignore} annotation.
*/
@Override
protected boolean isIgnored(FrameworkMethod child) {
return child.getAnnotation(Ignore.class) != null;
}
原来是在这里判断一个方法是否带有 @Ignore 注解
其他
画 “org.junit.runners.JUnit4 的简要类图” 所用到的代码
借助 PlantUML,可以用如下代码画出那张图
@startuml
title <i>org.junit.runners.JUnit4</i> 的简要类图
abstract class org.junit.runner.Runner
abstract class org.junit.runners.ParentRunner<T> {
- void runChildren(final RunNotifier notifier)
# {abstract} void runChild(T child, RunNotifier notifier)
}
class org.junit.runners.BlockJUnit4ClassRunner {
# void runChild(final FrameworkMethod method, RunNotifier notifier)
# boolean isIgnored(FrameworkMethod child)
}
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
note right of org.junit.runners.ParentRunner::runChildren
<code>
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
</code>
end note
note right of org.junit.runners.BlockJUnit4ClassRunner::runChild
<code>
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
Statement statement = new Statement() {
@Override
public void evaluate() throws Throwable {
methodBlock(method).evaluate();
}
};
runLeaf(statement, description, notifier);
}
}
</code>
end note
note right of org.junit.runners.BlockJUnit4ClassRunner::isIgnored
<code>
@Override
protected boolean isIgnored(FrameworkMethod child) {
return child.getAnnotation(Ignore.class) != null;
}
</code>
end note
@enduml
画“org.study.SimpleTest 的类图” 所用到的代码
借助 PlantUML,可以用如下代码画出那张图
@startuml
'https://plantuml.com/component-diagram
title <i>JUnit 4</i> 生成的部分对象
[<b><i>Suite</i></b> 的实例\n(这个实例的精确类型是\n<i>org.junit.runner.Computer$2</i>)] as suite #orange
[<b><i>TestClass</i></b> for <i>null</i>] as tc_null #pink
[<b><i>Runner</i></b> for <i>SimpleTest</i>\n(这个 <b><i>Runner</i></b> 的精确类型是\n<i>org.junit.runners.JUnit4</i>)] as r #orange
[<b><i>TestClass</i></b> for <i>SimpleTest</i>] as tc #pink
[<b><i>FrameworkMethod</i></b>\nfor <i>test1()</i> method] as m1 #lightblue
[<b><i>FrameworkMethod</i></b>\nfor <i>test2()</i> method] as m2 #lightblue
[<b><i>FrameworkMethod</i></b>\nfor <i>test3()</i> method] as m3 #lightblue
r ..> tc: 持有
suite --> r: 有一个 <i>child</i> 节点是它
suite ..> tc_null: 持有
r --> m1: 有一个 <i>child</i> 节点是它
r --> m2: 有一个 <i>child</i> 节点是它
r --> m3: 有一个 <i>child</i> 节点是它
legend right
橙色节点表示 <b><i>Runner</i></b> 的实例
粉色节点表示 <b><i>TestClass</i></b> 的实例
浅蓝色节点表示 <b><i>FrameworkMethod</i></b> 的实例
end legend
@enduml
画“JUnit 4 生成的部分对象”所用到的代码
借助 PlantUML,可以用如下代码画出那张图
@startuml
'https://plantuml.com/component-diagram
title <i>JUnit 4</i> 生成的部分对象
caption xx
[<b><i>Suite</i></b> 的实例\n(这个实例的精确类型是\n<i>org.junit.runner.Computer$2</i>)] as suite #orange
[<b><i>TestClass</i></b> for <i>null</i>] as tc_null #pink
[<b><i>Runner</i></b> for <i>SimpleTest</i>\n(这个 <b><i>Runner</i></b> 的精确类型是\n<i>org.junit.runners.JUnit4</i>)] as r #orange
[<b><i>TestClass</i></b> for <i>SimpleTest</i>] as tc #pink
[<b><i>FrameworkMethod</i></b>\nfor <i>test1()</i> method] as m1 #lightgreen
[<b><i>FrameworkMethod</i></b>\nfor <i>test2()</i> method] as m2 #lightgreen
[<b><i>FrameworkMethod</i></b>\nfor <i>test3()</i> method] as m3 #lightgreen
r ..> tc: 持有
suite --> r: 有一个 <i>child</i> 节点是它
suite ..> tc_null: 持有
r --> m1: 有一个 <i>child</i> 节点是它
r --> m2: 有一个 <i>child</i> 节点是它
r --> m3: 有一个 <i>child</i> 节点是它
@enduml