背景
JUnit 4 是著名的单元测试框架,它有以下注解
@Test@Before@After@BeforeClass@AfterClass@Ignore
如果请你来实现类似于 JUnit 4 功能(例如支持上述注解的前 6 个),你会如何着手呢?
可以通过 反射 去类里查找这些注解(如果做得通用些,可以查找一个类里出现的 所有 注解),然后基于查找的结果再做对应的处理。
这个查找动作可以是 一次性 的(即,对一个类只需要分析一次并将查找结果记录在某个地方),那就可以用某个专门的类来处理“分析”+“记录查找结果”的逻辑。
这个类就是本文的主角:
要点
TestClass 中的部分字段和方法如下图所示 ⬇️
TestClass 对测试类进行封装,它含有如下 3 个字段(上方的类图中也展示了这 3 个字段)
private final Class<?> clazzprivate final Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotationsprivate final Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations
它们的大致作用如下所示 ⬇️
| 字段名称 | 作用 | 例子 |
|---|---|---|
| 记录测试类的 类型信息 | (下文会提到这个类) | |
| 记录这个类中 注解 和 方法 的对应关系 | ||
| 记录这个类中 注解 和 字段 的对应关系 | (本文不涉及这样的例子) |
因此,借助 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 (在项目顶层)
执行以下 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 中的单元测试,其效果如下所示 ⬇️
从运行结果中可以看到,
- 带有
@Before注解的2个方法在testAdd()方法(这个方法带有@Test注解) 之前 运行 - 带有
@After注解的2个方法在testAdd()方法(这个方法带有@Test注解) 之后 运行
但是 JUnit 4 怎么知道哪些方法带有 @Before, @After, @Test 注解呢?这就要提到本文的主角了:
TestClass
分析
AdderTest.java 文件里有 main 方法(具体代码请参考上文),我们可以在 main 方法里的 int a = 42; 这一行(这一行没有特别的含义,只是为了找个地方观察 TestClass)打一个断点,以观察 TestClass
当我们 debug AdderTest 类中的 main 方法时,可以看到 testClass 变量中有一个 字段(如下图所示)。
它是一个 Map。点击 这个字段后,可以看到其中有三个 key 分别和以下注解对应
@Test@Before@After
再点开这三个 key-value 对,可以看到带有对应注解的方法都出现在对应的 value 中 ⬇️
由此可以猜测, 应该会分析指定类中各个方法上的注解,并将这些信息保存到了 字段中。
的构造函数如下图所示,看起来这个猜测是正确的。
我们把 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; 这一行打断点 ⬇️
再次 debug main 方法,可以看到如下的结果
小结
正如 TestClass.java 中的 javadoc 所述,TestClass 的作用是 ⬇️
Wraps a class to be run, providing method validation and annotation searching
它会将一个测试类(例如本文中的 AdderTest.class)进行包装,在此基础上我们可以对这个类中的注解进行搜索(例如搜索哪些方法带有 @Test 注解)。关于 TestClass 的作用,我胡乱联想了一番 ⬇️
Spring的BeanDefinition: 我们在用Spring时,Spring会把各种来源的Bean的描述封装成BeanDefinition,以便后续使用。我觉得TestClass也起到了类似的作用,即,封装某些信息以便后续使用(其实Spring与JUnit的定位完全不同,两者的相似之处很有限,这里只是强行类比一下)- 工具书: 在生活中,当我们获取到第一手资料时,它们可能并不好用(例如难懂的古籍),但我们可以借助工具书来使用这些资料了(例如借助词典来查询难懂的字词)
其他
本文“要点”一小节中的图是如何画出来的?
我是用 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