浅解 JUnit 4 第一篇: TestClass

102 阅读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 对测试类进行封装,它含有如下 3 个字段(上方的类图中也展示了这 3 个字段)

  • private final Class<?> clazz
  • private final Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations
  • private final Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations

它们的大致作用如下所示 ⬇️

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

image.png

因此,借助 TestClass 就可以查找到带有指定注解的方法/字段。

正文

用到的文件

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

  • src/main/java/ 目录下有以下文件
    • src/main/java/org/example/Adder.java
  • src/test/java 目录下有以下文件
    • src/test/java/org/study/AdderTest.java
  • pom.xml (在项目顶层)
image.png

执行以下 shell 命令,就可以生成上述目录/文件(文件为空,下文列出了 Adder.java/AdderTest.java/pom.xml 的内容,可以手动复制粘贴过去)

mkdir junit-study
cd junit-study

mkdir -p src/main/java/org/example/
mkdir -p src/test/java/org/study/

touch src/main/java/org/example/Adder.java
touch src/test/java/org/study/AdderTest.java
touch pom.xml

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 TestClass 中的部分字段和方法

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
    +List<FrameworkMethod> getAnnotatedMethods(Class<? extends Annotation> annotationClass)
    +List<FrameworkField> getAnnotatedFields(Class<? extends Annotation> annotationClass)
}

legend right
    掘金技术社区
    @金銀銅鐵
endlegend

@enduml

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

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

caption <i>TestClass</i> 中的字段和它们的作用

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

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

legend right
浅绿色的节点代表 <i>TestClass</i> 中的 <i>3</i> 个字段
endlegend
center footer 掘金技术社区@金銀銅鐵

@endmindmap