浅解 JUnit 4 第十篇:方法上的 @Ignore 注解

100 阅读3分钟

背景

在 浅解 JUnit 4 第四篇:类上的 @Ignore 注解 一文中,我们初步探讨了 类上的 @Ignore 注解 是如何起作用的。那么 方法上的 @Ignore 注解 又是如何起作用的呢?本文会对这个问题进行探讨。

要点

  • JUnit 4 会为测试类生成对应的 Runner
  • 对一个测试类 A\text{A} 而言,典型的情况为 ⬇️
    • JUnit 4 为其生成的 Runner:RunnerA\text{Runner}: \text{Runner}_\text{A}org.junit.runners.JUnit4 的实例
  • ParentRunner 会通过 runChildren(RunNotifier) 方法来查找所有子节点并运行对应的测试
    • runChildren(RunNotifier) 方法会调用 runChild(T, RunNotifier) 方法
    • BlockJUnit4ClassRunneroverriderunChild(FrameworkMethod, RunNotifier) 方法
      • BlockJUnit4ClassRunner 中的 runChild(FrameworkMethod, RunNotifier) 方法里会检查当前方法节点是否带有 @Ignore 注解,如果有,则略过这个方法

我把重要的类/方法在下图中展示出来了 ⬇️ f10.png

一些类的全限定类名

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

简略的类名全限定类名(Fully Qualified Class Name)
BlockJUnit4ClassRunnerorg.junit.runners.BlockJUnit4ClassRunner
Computer$2org.junit.runner.Computer$2
FrameworkMethodorg.junit.runners.model.FrameworkMethod
JUnit4org.junit.runners.JUnit4
ParentRunnerorg.junit.runners.ParentRunner
Runnerorg.junit.runner.Runner
Suiteorg.junit.runners.Suite
TestClassorg.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);
    }
}

对应的类图如下 ⬇️ image.png

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) 方法里打一个断点(断点的位置如下图所示)。为了便于描述,我们把这个断点称为 断点甲

image.png

第一次遇到断点甲

然后 debug SimpleTestmain 方法。当程序运行到 断点甲 这里时,可以观察到 this 的类型是 Computer$2

image.png

虽然我们只写了 SimpleTest 一个类,但是 JUnit 4 会为我们生成很多相关的对象,其中的一部分对象如下图所示 ⬇️

image.png

当程序第一次运行到 断点甲 那里时,可以观察到 getFilteredChildren() 方法所返回的 List 中只包含一个元素,而这个元素就是 SimpleTest 对应的 Runner ⬇️

image.png

第二次遇到断点甲

然后我们让程序继续运行,当程序第二次运行到 断点甲 那里时,可以观察到 getFilteredChildren() 方法所返回的 List 中包含 3 个元素,而这 3 个元素就是 SimpleTest 中的那三个带有 @Test 注解的方法对应的 FrameworkMethod 对象(可以简单将 FrameworkMethod 理解为让 方法(method) 更容易使用的一个辅助类)

image.png

既然调用 RunnerSimpleTest\text{Runner}_\text{SimpleTest} 上的 getFilteredChildren() 方法时,返回值中有 3 个元素,那么 test3() 这个方法为什么没有运行呢?

带有 @Ignore 注解的方法是什么时候被剔除的?

我们在 ParentRunner.java 的第 331 行再新增一个断点(断点的位置如下图所示)。为了便于描述,我们把这个新增的断点称为 断点乙

image.png

让程序继续运行到 断点乙 这里。然后 Step Into 这个方法,我们会来到 BlockJUnit4ClassRunner 类的 runChild(final FrameworkMethod method, RunNotifier notifier) 方法里。可以观察到 this 的类型是 JUnit4(这个类的全限定类名是 org.junit.runners.JUnit4),而 BlockJUnit4ClassRunner.java 的第 94 行看起来有点可疑 ⬇️

image.png

我们去看一下第 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;
}

isIgnored(FrameworkMethod child) 方法里会判断 child 参数所对应的方法是否带有 @Ignore 注解

  • 如果 child 参数所对应的方法 带有 @Ignore 注解,则对应的方法 不会被运行(如下图红框所示)
  • 如果 child 参数所对应的方法 没有 @Ignore 注解,则对应的方法 会被正常处理(如下图红线所示)

image.png

其他

画 “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