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

5 阅读3分钟

背景

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

要点

  • JUnit 4 会为测试类生成对应的 Runner
  • 对一个测试类 A\text{A} 而言,典型的情况为 ⬇️
    • JUnit 4 为其生成的 Runner: RunnerA\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

第一次遇到断点

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

image.png

第二次遇到断点

然后我们让程序继续运行,当程序第二次运行到 328 行的断点那里时,可以观察到 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;
}

原来是在这里判断一个方法是否带有 @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