背景
在 浅解 JUnit 4 第十二篇:如何实现一个 @Before 注解的替代品?(上) 一文中,我用一个小项目展示了如何实现一个 @Before 注解的替代品(即 @MyBefore 注解),但是没有解释 @MyBefore 注解具体是如何被处理的。本文我们会探讨这个问题。
要点
- 可以封装“运行测试方法”的逻辑,通过层层包装的 对象,我们可以实现各种对测试方法的 前置/后置/前置+后置 处理
- 通过实现 接口(或者实现 接口),我们就可以自由控制测试方法 执行前/执行后/执行前+执行后 的逻辑
一些类的全限定类名
文中提到 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 |
正文
一些思考
如果请你来设计 JUnit 4,并要求能够支持 @Before/@After 等注解(考虑到用户可能会自定义一些注解,以支持在测试方法执行 之前/之后/之前和之后 运行某些逻辑,我们的设计需要具有可扩展性)。那么你会怎么设计呢?
在 浅解 JUnit 4 第一篇: TestClass 一文中,我们分析过,借助 就可以知道测试类 中带有某个注解的方法/字段有哪些。如果想把 “在运行测试方法 之前/之后/之前和之后 运行某些逻辑” 的功能做得通用些,我们可以对 “运行某个测试方法” 进行封装。我们可以借鉴 Spring 里的 BeanPostProcessor 的思路。一个 Bean 可以被很多个 BeanPostProcessor 处理,用户甚至可以自己指定一些 BeanPostProcessor。这样可扩展性就非常好了(Bean 创建的大流程由 Spring 框架来掌控,但是某些环节的处理细节会交给具体的 BeanPostProcessor 来处理)。
JUnit 4 中,把 “运行某个测试方法” 封装成了 对象(其中只定义了 evaluate() 方法)。如果需要在 的某个实例 之前/之后/之前和之后 做些事情,那么只需要将 封装为 ,并保证 在调用 的 evaluate() 方法 之前/之后/之前+之后 做那些事情就行了。
下方的思维导图里举了一个简单的例子
项目结构
项目结构请参考 浅解 JUnit 4 第十二篇:如何实现一个 @Before 注解的替代品?(上) 一文的 一个具体的例子 那一小节。
原理分析
JUnit 4 对 @Before 注解的处理
JUnit 4 处理 @Before 注解的核心原理并不复杂,可以用下方的思维导图来简单概括
详细的分析可以参考 浅解 JUnit 4 第十一篇:@Before 注解和 @After 注解如何发挥作用? 一文。
JUnit 4 对 Rule 的处理
将 @Before/@After 注解进行推广,如果我们需要对 "运行测试方法" 这一逻辑做一些 前置/后置/前置+后置 处理,例如统计测试方法的运行时长,在测试方法运行前分配资源/在测试方法运行后释放资源,那么 JUnit 4 也是支持的。
在 org.junit.runners.BlockJUnit4ClassRunner 的 methodBlock(FrameworkMethod) 方法中,可以看到有一行代码会调用 withRules(FrameworkMethod, Object, Statement) 方法(代码的位置如下图所示 ⬇️)
withRules(FrameworkMethod, Object, Statement) 方法是这样的 ⬇️
private Statement withRules(FrameworkMethod method, Object target, Statement statement) {
RuleContainer ruleContainer = new RuleContainer();
CURRENT_RULE_CONTAINER.set(ruleContainer);
try {
List<TestRule> testRules = getTestRules(target);
for (MethodRule each : rules(target)) {
if (!(each instanceof TestRule && testRules.contains(each))) {
ruleContainer.add(each);
}
}
for (TestRule rule : testRules) {
ruleContainer.add(rule);
}
} finally {
CURRENT_RULE_CONTAINER.remove();
}
return ruleContainer.apply(method, describeChild(method), target, statement);
}
这个方法的核心逻辑看起来是以下三件事 ⬇️
我给这三件事画了一张思维导图 ⬇️
在 浅解 JUnit 4 第十二篇:如何实现一个 @Before 注解的替代品?(上) 一文中所展示的代码里,用的是 MethodRule
我们在 MyBeforeRule 类打一个断点(断点的位置如下图所示 ⬇️)。为了便于描述,我们把它简称为 断点甲 吧。
之后 debug SimpleAdderTest 的 main 方法。当程序运行到 断点甲 这里时,可以看到 base 变量以及 myBeforeMethods 变量的内容都符合预期(效果如下图所示 ⬇️)
用 PlantUML 画图所用到的代码
画 "MethodRule 和 TestRule 的简要类图" 一图所用到的代码
@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.MyBeforeRule
class org.study.rules.MyBeforeRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
@enduml
画 "一个简单的例子" 一图所用到的代码
@startmindmap
top to bottom direction
title 一个简单的例子
*:我们需要运行测试方法 <i>testMethod<sub>a</sub></i>
在运行它之前需要做一些准备工作(这些准备工作简称为 <b>事情甲</b>);
**:那么我们可以用两个 <i>Statement</i> 实例(即 <i>statement<sub>1</sub></i>, <i>statement<sub>2</sub></i>)
来做这些事情;
***_ <i>statement<sub>1</sub></i>
****[#lightblue] <i>statement<sub>1</sub></i> 的 <i>evaluate()</i> 方法所做的事情 <:point_down:>
*****[#lightblue] 运行 <i>testMethod<sub>1</sub></i>
***_ <i>statement<sub>2</sub></i>
****[#orange] <i>statement<sub>2</sub></i> 的 <i>evaluate()</i> 方法所做的事情 <:point_down:>
*****[#orange] <b>先</b> 做 <b>事情甲</b>
*****[#orange] <b>后</b> 调用 <i>statement<sub>1</sub></i> 的 <i>evaluate()</i> 方法
legend left
浅蓝色的节点和 <i>statement<sub>1</sub></i> 有关
橙色的节点和 <i>statement<sub>2</sub></i> 有关
end legend
@endmindmap
画 "JUnit 4 如何处理 @Before 注解?" 一图所用到的代码
@startmindmap
top to bottom direction
caption \n\n
' caption 中的内容只是为了方法掘金平台的水印遮盖图中的内容
title <i>JUnit 4</i> 如何处理 <i>@Before</i> 注解?
* <i>JUnit 4</i> 会将一个测试方法包装成若干层的 <i>Statement</i> 对象
**_:<i>Statement</i> 中只定义了一个 <i>evaluate()</i> 方法,
所以 <i>Statement</i> 很像一个 <i>fuctional interface</i>;
***:假设在测试类 <i>T</i> 中
<&star> <i>testMethod<sub>1</sub></i> 方法带有 <i>@Test</i> 注解
<&star> <i>init</i> 方法带有 <i>@Before</i> 注解;
****:把 <i>testMethod<sub>1</sub></i> 方法对应的原始的 <i>Statement</i> 对象称为 <i>statement<sub>1</sub></i>
<i>JUnit 4</i> 会把 <i>statement<sub>1</sub></i> 包装为 <i>statement<sub>2</sub></i>
而 <i>statement<sub>2</sub></i> 中的 <i>evaluate()</i> 方法的执行逻辑是 <:point_down:>;
*****[#lightblue] <&star> <b>先</b> (通过反射)调用 <i>init</i> 方法
*****[#lightgreen] <&star> <b>后</b> 调用 <i>statement<sub>1</sub></i> 的 <i>evaluate()</i> 方法
******[#lightgreen] <i>statement<sub>1</sub></i> 的 <i>evaluate()</i> 方法会(通过反射)调用 <i>testMethod<sub>1</sub></i> 方法
@endmindmap
画 "withRules(FrameworkMethod, Object, Statement) 方法的核心逻辑" 一图所用到的代码
@startmindmap
top to bottom direction
caption \n\n
' caption 中的 \n\n 只是为了防止掘金平台自动添加的水印遮盖图中的内容
' caption 的内容没有特别的含义
title <i>withRules(FrameworkMethod, Object, Statement)</i> 方法的核心逻辑
*:1. <b>获取</b>
获取带有 <i>@Rule</i> 注解的字段和方法
字段的类型应当是 <i>TestRule</i> 或者 <i>MethodRule</i>
方法的返回值的类型应当是 <i>TestRule</i> 或者 <i>MethodRule</i>;
*:2. <b>添加</b>
将满足要求的字段和方法添加到 <i>RuleContainer</i> 中
可以将 <i>RuleContainer</i> 简单理解成 <b>带有 <i>@Rule</i> 注解的字段和方法的容器</b>
<i>RuleContainer</i> 中有如下的两个字段 <:point_down:>
<&star> <i>List<TestRule> testRules</i>
<&star> <i>List<MethodRule> methodRules</i>
它们分别用于保存 <i>TestRule</i> 的元素和 <i>MethodRule</i> 的元素;
*:3. <b>应用</b>
* 遍历 <i>RuleContainer</i> 中的元素
** 如果遇到 <i>TestRule</i> 类型的元素
则调用 <i>TestRule</i> 中的 <i>apply(Statement, Description)</i> 方法
对入参中的 <i>Statement</i> 对象进行包装(也可以不包装)
** 如果遇到 <i>MethodRule</i> 类型的元素,
则调用 <i>MethodRule</i> 中的 <i>apply(Statement, FrameworkMethod, Object)</i> 方法
对入参中的 <i>Statement</i> 对象进行包装(也可以不包装);
@endmindmap