背景
在以下两篇文章中,我们探讨了 如何实现一个 @Before 注解的替代品 的问题。
在此基础上,再实现一个 @After 注解的替代品就比较容易了。本文会探讨如何实现一个 @After 注解的替代品。
要点
-
Statement 可以封装"运行测试方法"的逻辑,通过层层包装的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象,我们可以实现各种对测试方法的 前置/后置/前置+后置 处理
-
通过实现 <math xmlns="http://www.w3.org/1998/Math/MathML"> MethodRule \text{MethodRule} </math>MethodRule 接口(或者实现 <math xmlns="http://www.w3.org/1998/Math/MathML"> TestRule \text{TestRule} </math>TestRule 接口),我们就可以自由控制测试方法 执行前/执行后/执行前+执行后 的逻辑
-
<math xmlns="http://www.w3.org/1998/Math/MathML"> MyAfterRule \text{MyAfterRule} </math>MyAfterRule (这个类是我自己写的)作为 <math xmlns="http://www.w3.org/1998/Math/MathML"> MethodRule \text{MethodRule} </math>MethodRule 接口的实现类,其
apply(Statement base, FrameworkMethod method, Object target)方法中会查找测试类中有哪些方法带有@MyAfter注解。这个apply(Statement base, FrameworkMethod method, Object target)方法会返回一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象,这个 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象的evaluate()方法的执行逻辑是- 先 调用
base参数的evaluate()方法 - 后 (通过反射)调用测试类中带有
@MyAfter注解的方法
- 先 调用

一些类的全限定类名
文中提到 JUnit 4 中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.Rule 写成 @Rule)。我在这一小节把简略类名和全限定类名的对应关系列出来
| 简略的类名 | 全限定类名(Fully Qualified Class Name) |
|---|---|
After 或 @After |
org.junit.After |
Before 或 @Before |
org.junit.Before |
MethodRule |
org.junit.rules.MethodRule |
Rule 或 @Rule |
org.junit.Rule |
Statement |
org.junit.runners.model.Statement |
TestRule |
org.junit.rules.TestRule |
正文
目标
在使用 @After 注解时,我们预期的行为是什么呢?下方的思维导图里举了一个例子 ⬇️

所以我们的目标是是
- 实现一个类似
@After的注解(例如可以叫@MyAfter) - 在运行每一个测试方法后,让带有
@MyAfter注解的方法运行
先看第一个目标。
第一个目标:实现一个类似 @After 的注解
下图展示了 @After 注解的详情 ⬇️

可以看到 @After 注解
- 会保留到运行时
- 只能用在方法上
那么我们自己实现的 @MyAfter 注解也照做就行了,所以 @MyAfter 注解的核心内容会是这样 ⬇️ (这里略去了 package 语句和 import 语句)
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAfter {
}
但是只写一个注解并没有用,还需要能处理这个注解的代码
第二个目标:在运行每一个测试方法后,让带有 @MyAfter 注解的方法运行
浅解 JUnit 4 第十三篇:如何实现一个 @Before 注解的替代品?(下) 一文提到, JUnit 4 中定义了 MethodRule 接口和 TestRule 接口,只需要实现两个接口中的任意一个,就可以对"运行测试方法"的逻辑进行包装。JUnit 4 中,把 "运行某个测试方法" 封装成了 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象(其中只定义了 evaluate() 方法)。如果需要在 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 的某个实例 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t n statement_n </math>statementn 之后做些事情,那么只需要将 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t n statement_n </math>statementn 封装为 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t n + 1 statement_{n+1} </math>statementn+1,并保证 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t n + 1 statement_{n+1} </math>statementn+1 在调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t n statement_n </math>statementn 的 evaluate() 方法之后做那些事情就行了。
项目结构
我在本地创建了一个小项目来以便探讨本文的问题,这个项目的结构如下 ⬇️
text
.
├── pom.xml
└── src
├── main
│ └── java
│ └── org
│ └── example
│ └── SimpleAdder.java
└── test
└── java
└── org
└── study
├── annotations
│ └── MyAfter.java
├── rules
│ └── MyAfterRule.java
└── SimpleAdderTest.java
SimpleAdder.java
SimpleAdder.java 里是一个简陋的 adder 的代码 ⬇️
java
package org.example;
public class SimpleAdder {
public int add(int a, int b) {
return a + b;
}
}
MyAfter.java
MyAfter.java 里是我们实现的 @After 注解的替代品的代码 ⬇️
java
package org.study.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAfter {
}
MyAfterRule.java
MyAfterRule.java 里是处理 @MyAfter 注解的代码 ⬇️ (MyAfterRule 是 MethodRule 接口的一个实现类)
java
package org.study.rules;
import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
import org.study.annotations.MyAfter;
import java.util.List;
public class MyAfterRule implements MethodRule {
@Override
public Statement apply(Statement base, FrameworkMethod method, Object target) {
TestClass testClass = new TestClass(target.getClass());
List<FrameworkMethod> myAfterMethods = testClass.getAnnotatedMethods(MyAfter.class);
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
for (var myBeforeMethod : myAfterMethods) {
myBeforeMethod.invokeExplosively(target);
}
}
};
}
}
MyAfterRule 类的 apply(Statement, FrameworkMethod, Object) 方法会返回一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象。为了便于区分,我们把
- 入参中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t b a s e {statement_{base}} </math>statementbase
- 这个方法返回的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Statement \text{Statement} </math>Statement 对象称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t r e t u r n {statement_{return}} </math>statementreturn
那么 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t r e t u r n {statement_{return}} </math>statementreturn 中的 evaluate() 方法的执行逻辑就是 ⬇️
- 先 执行 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e m e n t b a s e {statement_{base}} </math>statementbase 的
evaluate()方法 - 后 执行
target对应的测试类(即SimpleAdderTest)上带有@MyAfter注解的各个方法
SimpleAdderTest.java
SimpleAdderTest.java 里是一个测试类的代码 ⬇️
java
package org.study;
import org.example.SimpleAdder;
import org.junit.*;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.study.annotations.MyAfter;
import org.study.rules.MyAfterRule;
import java.util.Random;
public class SimpleAdderTest {
private final Random random = new Random();
private static final int BOUND = 10;
private int a;
private int b;
private int expectedResult;
private final SimpleAdder adder = new SimpleAdder();
@Rule
public final MyAfterRule myAfterRule = new MyAfterRule();
@MyAfter
public void postTest() {
String description = String.format("SimpleAdder 的计算结果满足: %s = %s + %s", expectedResult, a, b);
System.out.println(description);
}
@Test
public void testAdd() {
a = random.nextInt(BOUND);
b = random.nextInt(BOUND);
expectedResult = a + b;
Assert.assertEquals(expectedResult, adder.add(a, b));
System.out.println("testAdd() 方法即将返回");
}
public static void main(String[] args) {
Result result = JUnitCore.runClasses(SimpleAdderTest.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure);
}
}
}
pom.xml
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>
项目中出现的各个类的类图如下 ⬇️

运行结果
我们运行 SimpleAdderTest 中的 main 方法,应该可以看到类似这样的结果 ⬇️

其中 红色框 的内容和 testAdd() 方法有关,黄色框 的内容和 postRun() 方法有关。由于 a/b 这两个数字是随机生成的,所以你自己运行时,看到的 a/b 可能是其他值。
简单的分析
我们在 MyAfterRule 类的 apply(Statement, FrameworkMethod, Object) 方法里打一个断点(断点的位置如下图所示 ⬇️)。为了便于描述,我们将这个断点称为 断点甲

然后 debug SimpleAdderTest 的 main 方法。当程序运行到 断点甲 这里时,可以观察到
method变量和SimpleAdderTest类里的testAdd()方法对应myAfterMethods变量和SimpleAdderTest类里所有带有@MyAfter注解的方法对应(SimpleAdderTest类里只有postRun()方法带有@MyAfter注解)

其他
用 PlantUML 画图,所用到的代码
画 "MethodRule 和 TestRule 的简要类图" 一图所用到的代码
puml
@startuml
title <i>MethodRule</i> 和 <i>TestRule</i> 的简要类图
interface org.junit.rules.MethodRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
interface org.junit.rules.TestRule {
+ Statement apply(Statement base, Description description)
}
annotation org.junit.Rule {
}
org.junit.rules.MethodRule <|.. org.study.rules.MyAfterRule
class org.study.rules.MyAfterRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
note right of org.study.rules.MyAfterRule::apply
<code>
@Override
public Statement apply(Statement base, FrameworkMethod method, Object target) {
TestClass testClass = new TestClass(target.getClass());
List<FrameworkMethod> myAfterMethods = testClass.getAnnotatedMethods(MyAfter.class);
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
for (var myBeforeMethod : myAfterMethods) {
myBeforeMethod.invokeExplosively(target);
}
}
};
}
</code>
<i>apply(Statement base, FrameworkMethod method, Object target)</i> 方法会返回一个 <i>Statement</i> 对象,
在这个 <i>Statement</i> 对象的 <i>evaluate()</i> 方法中会 <:point_down:>
* **先** 调用 <i>base</i> 参数的 <i>evaluate()</i> 方法
* **后** (通过反射)调用测试类中带有 <i>@MyAfter</i> 注解的方法
end note
@enduml
画 "使用 @After 注解时, 我们预期的行为是什么?" 一图所用到的代码
puml
@startmindmap
top to bottom direction
title 使用 <i>@After</i> 注解时,\n我们预期的行为是什么?
*:假设一个测试类 <i>T</i> 里
有 <i>2</i> 个带有 <i>@Test</i> 注解的方法: <i>testMethod<sub>1</sub>, testMethod<sub>2</sub></i>
有 <i>1</i> 个带有 <i>@After</i> 注解的方法: <i>postRun</i>;
**_ 那么我们对 <i>JUnit 4</i> 的预期是
*** 在运行 <i>testMethod<sub>1</sub> 方法 <b>之后</b> 要运行 <i>postRun</i> 方法
*** 在运行 <i>testMethod<sub>2</sub> 方法 <b>之后</b> 要运行 <i>postRun</i> 方法
@endmindmap
画 "类图" 一图所用到的代码
puml
@startuml
'https://plantuml.com/class-diagram
title 类图
annotation org.study.annotations.MyAfter
class org.example.SimpleAdder {
+ int add(int a, int b)
}
interface org.junit.rules.MethodRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
class org.study.rules.MyAfterRule
org.junit.rules.MethodRule <|.. org.study.rules.MyAfterRule
class org.study.rules.MyAfterRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
class org.study.SimpleAdderTest {
- final Random random
- {static} final int BOUND
- int a
- int b
- int expectedResult
- final SimpleAdder adder
+ final MyAfterRule myAfterRule
+ void postTest()
+ void testAdd()
+ {static} void main(String[] args)
}
@enduml