浅解 JUnit 4 第一篇: TestClass

139 阅读5分钟

背景

JUnit 4 是著名的单元测试框架,它有以下注解

  • @Test
  • @Before
  • @After
  • @BeforeClass
  • @AfterClass
  • @Ignore
  • \cdots

如果请你来实现类似于 JUnit 4 功能(例如支持上述注解的前 6 个),你会如何着手呢?

可以通过 反射 去类里查找这些注解(如果做得通用些,可以查找一个类里出现的 所有 注解),然后基于查找的结果再做对应的处理。

这个查找动作可以是 一次性 的(即,对一个类只需要分析一次并将查找结果记录在某个地方),那就可以用某个专门的类来处理“分析”+“记录查找结果”的逻辑。

这个类就是本文的主角:org.junit.runners.model.TestClass\text{org.junit.runners.model.TestClass}

要点

TestClass 的简要类图如下所示 ⬇️

image.png

TestClass 对测试类进行封装,上方的类图中展示了 TestClass 中的 3 个字段, 它们的大致作用是 ⬇️

字段名称作用例子
clazz\text{clazz}记录测试类的 类型信息AdderTest.class\text{AdderTest.class}
(下文会提到这个类)
methodsForAnnotations\text{methodsForAnnotations}记录这个类中 注解方法 的对应关系@Test[method1,method2,]\text{@Test}\to [method_1, method_2, \cdots]
fieldsForAnnotations\text{fieldsForAnnotations}记录这个类中 注解字段 的对应关系@Rule[field1,field2,]\text{@Rule}\to [field_1, field_2, \cdots] (本文不涉及这样的例子)

对应的思维导图如下 👇

image.png

因此,对测试类 T\text{T} 而言,借助和 T\text{T} 对应的 TestClass:TestClassT\text{TestClass}: \text{TestClass}_\text{T} 就可以查到带有指定注解的方法/字段。

正文

用到的文件

我在本地创建了一个小项目来以便于学习 JUnit 4,这个项目中包含以下目录/文件(.idea 目录和 target 目录的内容均略去)

.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── org
    │           └── example
    │               └── Adder.java
    └── test
        └── java
            └── org
                └── study
                    └── AdderTest.java

pom.xml

pom.xml 文件内容如下(因为我们只关注 JUnit 4,所以只有 JUnit 4 的依赖)

<?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>

Adder.java

Adder.java 的内容如下

package org.example;

public class Adder {
    public int add(int a, int b) {
        return a + b;
    }
}

AdderTest.java

AdderTest.java 的内容如下

package org.study;

import org.example.Adder;
import org.junit.*;
import org.junit.runners.model.TestClass;

public class AdderTest {

    private final Adder adder = new Adder();

    @Test
    public void testAdd() {
        System.out.println("Let's observe what we will get for 1 + 1");
        int result = adder.add(1, 1);
        Assert.assertEquals(2, result);
        System.out.println("The result is: " + result);
    }

    @Ignore
    @Test
    public void ignoredTest() {
        System.out.println("Hello world");
    }

    @Before
    public void beforeMethod1() {
        System.out.println("Hi, we are in a @Before-annotated method: beforeMethod1");
    }

    @Before
    public void beforeMethod2() {
        System.out.println("Hi, we are in a @Before-annotated method: beforeMethod2");
    }

    @After
    public void afterMethod1() {
        System.out.println("Hi, we are in an @After-annotated method: afterMethod1");
    }

    @After
    public void afterMethod2() {
        System.out.println("Hi, we are in an @After-annotated method: afterMethod2");
    }

    public static void main(String[] args) {
        TestClass testClass = new TestClass(AdderTest.class);
        int a = 42;
    }
}

Intellij IDEA (Community Edition) 里可以运行 AdderTest 中的单元测试,其效果如下所示 ⬇️

image.png

从运行结果中可以看到,

  • 带有 @Before 注解的 2 个方法在 testAdd() 方法(这个方法带有 @Test 注解) 之前 运行
  • 带有 @After 注解的 2 个方法在 testAdd() 方法(这个方法带有 @Test 注解) 之后 运行

但是 JUnit 4 怎么知道哪些方法带有 @Before, @After, @Test 注解呢?这就要提到本文的主角了: org.junit.runners.model.TestClass\text{org.junit.runners.model.TestClass}

TestClass

分析

AdderTest.java 文件里有 main 方法(具体代码请参考上文),我们可以在 main 方法里的 int a = 42; 这一行(这一行没有特别的含义,只是为了找个地方观察 TestClass)打一个断点,以观察 TestClass

image.png

当我们 debug AdderTest 类中的 main 方法时,可以看到 testClass 变量中有一个 methodsForAnnotations\text{methodsForAnnotations} 字段(如下图所示)。

image.png

它是一个 Map。点击 methodsForAnnotations\text{methodsForAnnotations} 这个字段后,可以看到其中有三个 key 分别和以下注解对应

  • @Test
  • @Before
  • @After

image.png

再点开这三个 key-value 对,可以看到带有对应注解的方法都出现在对应的 value 中 ⬇️

image.png

由此可以猜测,org.junit.runners.model.TestClass\text{org.junit.runners.model.TestClass} 应该会分析指定类中各个方法上的注解,并将这些信息保存到 methodsForAnnotations\text{methodsForAnnotations} 字段中。 org.junit.runners.model.TestClass\text{org.junit.runners.model.TestClass} 的构造函数如下图所示,看起来这个猜测是正确的。 image.png

我们把 main 方法的代码稍作调整 ⬇️

public static void main(String[] args) {
    TestClass testClass = new TestClass(AdderTest.class);
    List<FrameworkMethod> annotatedMethods = testClass.getAnnotatedMethods(Before.class);
    int a = 42;
}

还是在 int a = 42; 这一行打断点 ⬇️ image.png

再次 debug main 方法,可以看到如下的结果

image.png

小结

正如 TestClass.java 中的 javadoc 所述,TestClass 的作用是 ⬇️

Wraps a class to be run, providing method validation and annotation searching

image.png

它会将一个测试类(例如本文中的 AdderTest.class)进行包装,在此基础上我们可以对这个类中的注解进行搜索(例如搜索哪些方法带有 @Test 注解)。关于 TestClass 的作用,我胡乱联想了一番 ⬇️

  • SpringBeanDefinition: 我们在用 Spring 时,Spring 会把各种来源的 Bean 的描述封装成 BeanDefinition,以便后续使用。我觉得 TestClass 也起到了类似的作用,即,封装某些信息以便后续使用(其实 SpringJUnit 的定位完全不同,两者的相似之处很有限,这里只是强行类比一下)
  • 工具书: 在生活中,当我们获取到第一手资料时,它们可能并不好用(例如难懂的古籍),但我们可以借助工具书来使用这些资料了(例如借助词典来查询难懂的字词)

其他

本文“要点”一小节中的图是如何画出来的?

我是用 PlantUML 画的,具体的代码如下 ⬇️

画第一张图用到的代码 ⬇️

@startuml
'https://plantuml.com/class-diagram

caption 注意: 图中只列出了本文关心的字段和方法
title <i>org.junit.runners.model.TestClass</i> 的简要类图

class org.junit.runners.model.TestClass {
    -final Class<?> clazz
    -final Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations
    -final Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations
    +TestClass(Class<?> clazz)
    +List<FrameworkMethod> getAnnotatedMethods(Class<? extends Annotation> annotationClass)
    +List<FrameworkField> getAnnotatedFields(Class<? extends Annotation> annotationClass)
}

note top of org.junit.runners.model.TestClass
<i>javadoc</i> for <i>TestClass</i>:
<code>
Wraps a class to be run, providing method validation and annotation searching
</code>
end note

note left of org.junit.runners.model.TestClass::TestClass
<code>
Creates a {@code TestClass} wrapping {@code clazz}. Each time this
constructor executes, the class is scanned for annotations, which can be
an expensive process (we hope in future JDK's it will not be.) Therefore,
try to share instances of {@code TestClass} where possible.
</code>
end note

note left of org.junit.runners.model.TestClass::getAnnotatedMethods
<code>
Returns, efficiently, all the non-overridden methods in this class and
its superclasses that are annotated with {@code annotationClass}.
</code>
end note

note left of org.junit.runners.model.TestClass::getAnnotatedFields
<code>
Returns, efficiently, all the non-overridden fields in this class and its
superclasses that are annotated with {@code annotationClass}.
</code>
end note

@enduml

画第二张图用到的代码 ⬇️

@startmindmap
'https://plantuml.com/mindmap-diagram

top to bottom direction

title <i>TestClass</i> 中的 <i>3</i> 个重要字段和它们的作用

* 某个测试类 <i>T</i>
** <i>T</i> 对应的 <i>org.junit.runners.model.TestClass</i>
*** <i>TestClass</i> 中的 <i>3</i> 个重要字段 <:point_down:>
****[#lightgreen] <&star> <i>clazz</i> 字段
*****[#yellow] 记录 <i>T</i> 的类型信息
****[#lightgreen] <&star> <i>methodsForAnnotations</i> 字段
*****[#yellow] 记录 <i>T</i> 中 <b>注解</b> 和 <b>方法</b> 的对应关系

****[#lightgreen] <&star> <i>fieldsForAnnotations</i> 字段
*****[#yellow] 记录 <i>T</i> 中 <b>注解</b> 和 <b>字段</b> 的对应关系

footer \n\n
' 上面这一行只是为了防止掘金的水印遮住图片里的文字

@endmindmap