背景
在 浅解 JUnit 4 第十一篇:@Before 注解和 @After 注解如何发挥作用? 一文中,我们初步探讨了 @Before 注解 和@After 注解是如何发挥作用的。那么我们能否自己实现一个 @Before 注解的替代品呢?(@After 注解和 @Before 注解的功能是对称的,如果能写出 @Before 注解的替代品,那么应该也能写出 @After 注解的替代品)
这个问题涉及的内容比较多,本文只是上篇。下篇是 浅解 JUnit 4 第十三篇:如何实现一个 @Before 注解的替代品?(下)
正文
目标
在使用 @Before 注解时,我们预期的行为是什么呢?下方的思维导图里举了一个例子 ⬇️
所以我们的目标是是
- 实现一个类似
@Before的注解(例如可以叫@MyBefore) - 在运行每一个测试方法前,先让带有
@MyBefore注解的方法运行
先看第一个目标。
第一个目标:实现一个类似 @Before 的注解
下图展示了 @Before 注解的详情 ⬇️
可以看到 @Before 注解
- 会保留到运行时
- 只能用在方法上
那么我们自己实现的 @MyBefore 注解也照做就行了,所以 @MyBefore 注解的核心内容会是这样 ⬇️ (这里略去了 package 语句和 import 语句)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBefore {
}
但是只写一个注解并没有用,还需要能处理这个注解的代码
第二个目标:在运行每一个测试方法前,先让带有 @MyBefore 注解的方法运行
在 浅解 JUnit 4 第十一篇:@Before 注解和 @After 注解如何发挥作用? 一文中提到,@Before 注解是在 org.junit.runners.BlockJUnit4ClassRunner 类的 methodBlock(FrameworkMethod ) 方法里被处理的。methodBlock(FrameworkMethod ) 方法里做了很多事情,其中一件事情是调用 withBefores(FrameworkMethod, Object, Statement) 方法 ⬇️
如果测试类 中包含带有 @Before 注解的方法,那么在 withBefores(FrameworkMethod, Object, Statement) 方法中,入参中的 对象就会被包装为 对象( 是 的一个子类)。这就是处理 @Before 注解的核心逻辑。
那么在哪里可以处理我们自己定义的 @MyBefore 注解呢?在 withRules(FrameworkMethod, Object, Statement) 方法中可以做这件事 ⬇️
withRules(FrameworkMethod, Object, Statement) 方法涉及的内容比较多,在后面的文章我们再展开讲其中的细节。我们结合一个具体的小项目来看看如何实现第二个目标
一个具体的例子
我在本地创建了一个小项目来以便探讨本文的问题,这个项目的结构如下 ⬇️
.
├── pom.xml
└── src
├── main
│ └── java
│ └── org
│ └── example
│ └── SimpleAdder.java
└── test
└── java
└── org
└── study
├── annotations
│ └── MyBefore.java
├── rules
│ └── MyBeforeRule.java
└── SimpleAdderTest.java
SimpleAdder.java
SimpleAdder.java 的内容如下 ⬇️ 它的功能虽然很简单,但是用它来探讨本文的问题,也够用了。
package org.example;
public class SimpleAdder {
public int add(int a, int b) {
return a + b;
}
}
MyBefore.java
MyBefore.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;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBefore {
}
我们会把 @MyBefore 作为 @Before 的替代品,而它的内容可以照抄 @Before,注意需要修改 package。
MyBeforeRule.java
MyBeforeRule.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.MyBefore;
import java.util.List;
public class MyBeforeRule implements MethodRule {
@Override
public Statement apply(Statement base, FrameworkMethod method, Object target) {
TestClass testClass = new TestClass(target.getClass());
List<FrameworkMethod> myBeforeMethods = testClass.getAnnotatedMethods(MyBefore.class);
return new Statement() {
@Override
public void evaluate() throws Throwable {
for (var myBeforeMethod : myBeforeMethods) {
myBeforeMethod.invokeExplosively(target);
}
base.evaluate();
}
};
}
}
MyBeforeRule 用于处理 @MyBefore 注解
SimpleAdderTest.java
SimpleAdderTest.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.MyBefore;
import org.study.rules.MyBeforeRule;
import java.util.Random;
public class SimpleAdderTest {
private int a;
private int b;
private final SimpleAdder adder = new SimpleAdder();
private static final int BOUND = 10;
@Rule
public final MyBeforeRule myBeforeRule = new MyBeforeRule();
@MyBefore
public void prepare() {
Random random = new Random();
a = random.nextInt(BOUND);
b = random.nextInt(BOUND);
System.out.printf("Randomly generated a is: %s%n", a);
System.out.printf("Randomly generated b is: %s%n", b);
}
@Test
public void testAdd() {
int expectedResult = a + b;
Assert.assertEquals(expectedResult, adder.add(a, b));
String description = String.format("%s = %s + %s", expectedResult, a, b);
System.out.println(description);
}
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 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 方法,应该可以看到类似这样的结果 ⬇️
其中 红色框 的内容和 prepare() 方法有关,黄色框 的内容和 testAdd() 方法有关。由于 a/b 这两个数字是随机生成的,所以你自己运行时,看到的 a/b 可能是其他值。
这样看来,我们的确实现了一个 @Before 注解的替代品。但是这背后发生了什么呢?请看 浅解 JUnit 4 第十三篇:如何实现一个 @Before 注解的替代品?(下)
其他
用 PlantUML 画图,所用到的代码
画 “使用 @Before 注解时,\n我们预期的行为是什么?” 所用的代码
@startmindmap
top to bottom direction
title 使用 <i>@Before</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>@Before</i> 注解的方法: <i>prepare</i>;
**_ 那么我们对 <i>JUnit 4</i> 的预期是
*** 在运行 <i>testMethod<sub>1</sub> 方法 <b>之前</b> 要运行 <i>prepare</i> 方法
*** 在运行 <i>testMethod<sub>2</sub> 方法 <b>之前</b> 要运行 <i>prepare</i> 方法
@endmindmap
画 “类图” 所用的代码
@startuml
'https://plantuml.com/class-diagram
title 类图
annotation org.study.annotations.MyBefore
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.MyBeforeRule
org.junit.rules.MethodRule <|.. org.study.rules.MyBeforeRule
class org.study.rules.MyBeforeRule {
+ Statement apply(Statement base, FrameworkMethod method, Object target)
}
class org.study.SimpleAdderTest {
- int a;
- int b;
- final SimpleAdder adder
- {static} final int BOUND
+ final MyBeforeRule myBeforeRule
+ void prepare()
+ void testAdd()
+ {static} void main(String[] args)
}
@enduml