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

背景

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

要点

  • JUnit 4 会为测试类生成对应的 Runner
  • 对一个测试类 <math xmlns="http://www.w3.org/1998/Math/MathML"> A \text{A} </math>A 而言,典型的情况为 ⬇️
    • JUnit 4 为其生成的 Runner: <math xmlns="http://www.w3.org/1998/Math/MathML"> Runner A \text{Runner}_\text{A} </math>RunnerA 是 org.junit.runners.JUnit4 的实例
  • ParentRunner 会通过 runChildren(RunNotifier) 方法来查找所有子节点并运行对应的测试
    • runChildren(RunNotifier) 方法会调用 runChild(T, RunNotifier) 方法
    • BlockJUnit4ClassRunneroverriderunChild(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

正文

一个具体的例子

我在本地创建了一个小项目来以便探讨本文的问题,这个项目的结构如下 ⬇️

text 复制代码
├── pom.xml
└── src
    └── test
        └── java
            └── org
                └── study
                    └── SimpleTest.java

其中 SimpleTest.java 文件的内容如下 ⬇️

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 复制代码
<?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 SimpleTestmain 方法。当程序运行到 断点甲 这里时,可以观察到 this 的类型是 Computer$2

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

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

第二次遇到断点甲

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

既然调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> Runner SimpleTest \text{Runner}_\text{SimpleTest} </math>RunnerSimpleTest 上的 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) 方法。我把这个方法的代码复制到下方了

java 复制代码
/**
 * 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,可以用如下代码画出那张图

puml 复制代码
@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,可以用如下代码画出那张图

puml 复制代码
@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,可以用如下代码画出那张图

puml 复制代码
@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
相关推荐
阿狸猿2 天前
单元测试中静态测试、动态测试及白盒测试、回归测试实践
单元测试·软考
Max_uuc2 天前
【工程心法】从“在板盲调”到“云端验证”:嵌入式单元测试与 TDD 的工程化革命
单元测试·tdd
feathered-feathered3 天前
测试实战【用例设计】自己写的项目+功能测试(1)
java·服务器·后端·功能测试·jmeter·单元测试·压力测试
测试渣3 天前
持续集成中的自动化测试框架优化实战指南
python·ci/cd·单元测试·自动化·pytest
minh_coo3 天前
Spring单元测试之反射利器:ReflectionTestUtils
java·后端·spring·单元测试·intellij-idea
金銀銅鐵4 天前
浅解 JUnit 4 第九篇:JUnitCore (下)
junit·单元测试
钟智强4 天前
CVE-2025-49844高危预警:Redis Lua脚本引擎UAF漏洞深度剖析与POC实战
数据库·redis·web安全·junit·lua
A懿轩A4 天前
【Maven 构建工具】Maven + JUnit5 单元测试实战:测试级别、注解、断言与 Maven test 阶段
java·单元测试·maven