背景
JUnit 4 是著名的单元测试框架,它有以下注解
@Test@Before@After@BeforeClass@AfterClass@Ignore
如果请你来实现类似于 JUnit 4 功能(例如支持上述注解的前 6 个),你会如何着手呢?
可以通过 反射 去类里查找这些注解(如果做得通用些,可以查找一个类里出现的 所有 注解),然后基于查找的结果再做对应的处理。
这个查找动作可以是 一次性 的(即,对一个类只需要分析一次并将查找结果记录在某个地方),那就可以用某个专门的类来处理“分析”+“记录查找结果”的逻辑。
这个类就是本文的主角:
要点
TestClass 的简要类图如下所示 ⬇️
TestClass 对测试类进行封装,上方的类图中展示了 TestClass 中的 3 个字段,
它们的大致作用是 ⬇️
| 字段名称 | 作用 | 例子 |
|---|---|---|
| 记录测试类的 类型信息 | (下文会提到这个类) | |
| 记录这个类中 注解 和 方法 的对应关系 | ||
| 记录这个类中 注解 和 字段 的对应关系 | (本文不涉及这样的例子) |
对应的思维导图如下 👇
因此,对测试类 而言,借助和 对应的 就可以查到带有指定注解的方法/字段。
正文
用到的文件
我在本地创建了一个小项目来以便于学习 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 中的单元测试,其效果如下所示 ⬇️
从运行结果中可以看到,
- 带有
@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 <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