背景
在 浅解 JUnit 4 第十五篇:如何在测试方法运行前后做些事情? 一文中,我们探讨了 如何在测试方法运行前后做些事情 的问题。可以这样概括 ⬇️
- 在 接口的实现类中,可以自由控制测试方法运行前后要执行的逻辑。
- 通过
override接口的apply(Statement base, Description description)方法,可以将 包装为 。 里的evaluate()方法决定何时调用 的evaluate()方法
在此基础上,我们可以想到,有不少场景都需要“观察(而非影响)测试方法的运行”的行为,例如
- 在测试方法运行前后打印固定的日志
- 统计测试方法运行的耗时
- 在测试方法运行前后分配和释放资源
既然这样的场景很常见,那么在这些场景中,JUnit 4 中能否提供比 TestRule 更好用的类/接口呢?答案是肯定的。 就可以用于这些场景。
本文的主角是
要点
-
可以封装“运行测试方法”的逻辑,通过使用层层包装的 对象,我们可以实现各种对测试方法的 前置/后置/前置+后置 处理
-
通过实现 接口(或者实现 接口),我们就可以自由控制测试方法 执行前/执行后/执行前+执行后 的逻辑
-
(这个类是我自己写的)作为 的子类,间接实现了 接口, 会负责在测试方法运行的各个时机(例如 开始前/成功后/跳过后/失败后/结束时)调用相应的方法,由于本文的例子里只关心 开始前/结束时 这两个时机,我们只需要
override对应的方法(即starting(Description description)方法和finished(Description description)方法),就能做到如下的效果 ⬇️- 首先 输出星宿派的一个口号
- 其次 调用
testAdd()方法 - 最后 输出星宿派的一个口号
正文
的 javadoc
我们先看看 的 javadoc ⬇️
javadoc 里提到了三部分内容
第一部分
TestWatcher is a base class for Rules that take note of the testing action, without modifying it.
有些 Rule 只需要对测试行为做记录(而不用修改测试行为),TestWatcher 可以作为这样的类的基类。
第二部分
第二部分用代码展示了一个例子,我把代码复制到下方了 (我调了调缩进的风格)
public static class WatchmanTest {
private static String watchedLog;
@Rule(order = Integer.MIN_VALUE)
public TestWatcher watchman= new TestWatcher() {
@Override
protected void failed(Throwable e, Description description) {
watchedLog+= description + "\n";
}
@Override
protected void succeeded(Description description) {
watchedLog+= description + " " + "success!\n";
}
};
@Test
public void fails() {
fail();
}
@Test
public void succeeds() {
}
}
第三部分
It is recommended to always set the order of the TestWatcher to Integer.MIN_VALUE so that it encloses all other rules. Otherwise it may see failed tests as successful and vice versa if some rule changes the result of a test (e.g. ErrorCollector or ExpectedException).
第三部分建议总是将 的 order 设置为 Integer.MIN_VALUE,这样就能保证它能观测到最外层的"成功"/"失败"(因为内层的 Rule 可以将"成功"调整为"失败",或者将"失败"调整为"成功")。
order 是 @Rule 注解中的一个方法 ⬇️
一个具体的例子
和 浅解 JUnit 4 第十五篇:如何在测试方法运行前后做些事情? 一文一样,我们还是假设《天龙八部》里的星宿派的某个弟子需要写点单元测试,这位弟子想到如果能在测试方法运行前后都输出 星宿老仙 法力無邊 这个口号就好了。现在我们需要帮助这位弟子。
项目结构如下
.
├── pom.xml
└── src
├── main
│ └── java
│ └── org
│ └── example
│ └── SimpleAdder.java
└── test
└── java
└── org
└── study
├── rules
│ └── StarSectRule.java
└── SimpleAdderTest.java
SimpleAdder.java 的内容如下 ⬇️
package org.example;
public class SimpleAdder {
public int add(int a, int b) {
return a + b;
}
}
StarSectRule.java 的内容如下 ⬇️
package org.study.rules;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
/**
* A {@link TestRule} that is designed for Star Sect members.
* 专门给星宿派弟子用的 {@link TestRule}
*/
public class StarSectRule extends TestWatcher {
@Override
protected void starting(Description description) {
System.out.println("[測試前] 星宿老仙 法力無邊");
}
@Override
protected void finished(Description description) {
System.out.println("[測試後] 星宿老仙 法力無邊");
}
}
SimpleAdderTest.java 的内容如下 ⬇️
package org.study;
import org.example.SimpleAdder;
import org.junit.*;
import org.junit.rules.TestRule;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.study.rules.StarSectRule;
public class SimpleAdderTest {
private final SimpleAdder adder = new SimpleAdder();
@Rule(order = Integer.MIN_VALUE)
public TestRule buildTestRule() {
return new StarSectRule();
}
@Test
public void testAdd() {
int a = 1;
int b = 2;
int expectedResult = a + b;
Assert.assertEquals(expectedResult, adder.add(a, b));
System.out.printf("adder.add(%s, %s) = %s + %s = %s%n", a, b, a, b, expectedResult);
}
public static void main(String[] args) {
Result result = JUnitCore.runClasses(SimpleAdderTest.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure);
}
}
}
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 方法,应该能看到下图所示的效果 ⬇️
其中 红色框 的内容和 testAdd() 方法有关,黄色框 的内容和 前置/后置 处理有关。
简单的分析
如果想探索一下 是如何起作用的,那么可以在 里打一个断点(断点的位置如下图所示)
为了便于描述,我们将这个断点简称为 断点甲
我们 debug SimpleAdderTest 的 main 方法,当程序运行到 断点甲 这里时,可以看到如下的函数调用栈 ⬇️
可见, TestWatcher 使用了模版方法这种设计模式(即基类中定好整体的流程,但是允许子类 override 其中的某些方法)。更多细节,这里就不展开了,各位可以自己打断点研究。核心的点还是以下几个
- 通过 可以查到测试类 中哪些方法/字段带有指定的注解(例如
@Test/@Before/@After/@Rule/@Ignore) - 通过各种 可以封装“运行测试”的逻辑
- 通过各种 可以构建相应的
- 通过 可以封装“运行一个测试方法”的逻辑
- 通过使用层层包装的 对象,可以自由控制运行测试方法前后要做什么
- 接口中定义了
apply(Statement base, Description description)方法,我们可以在它的实现类里完成 的 前置/后置/前置+后置 处理逻辑
其他
用 PlantUML 画图,所用到的代码
画 "org.study.rules.StarSectRule 的简要类图" 一图所用到的代码
@startuml
title <i>org.study.rules.StarSectRule</i> 的简要类图
annotation org.junit.Rule {
}
interface org.junit.rules.TestRule {
+ Statement apply(Statement base, Description description)
}
abstract org.junit.rules.TestWatcher
org.junit.rules.TestRule <|.. org.junit.rules.TestWatcher
org.junit.rules.TestWatcher <|-- org.study.rules.StarSectRule
abstract class org.junit.rules.TestWatcher {
+ Statement apply(final Statement base, final Description description)
- void succeededQuietly(Description description, List<Throwable> errors)
- void failedQuietly(Throwable e, Description description, List<Throwable> errors)
- void skippedQuietly(org.junit.internal.AssumptionViolatedException e, Description description, List<Throwable> errors)
- void startingQuietly(Description description, List<Throwable> errors)
- void finishedQuietly(Description description, List<Throwable> errors)
# void succeeded(Description description)
# void failed(Throwable e, Description description)
# void skipped(AssumptionViolatedException e, Description description)
# void skipped(org.junit.internal.AssumptionViolatedException e, Description description)
# void starting(Description description)
# void finished(Description description)
}
class org.study.rules.StarSectRule {
# void starting(Description description)
# void finished(Description description)
}
note left of org.study.rules.StarSectRule::starting
<code>
@Override
protected void starting(Description description) {
System.out.println("[測試前] 星宿老仙 法力無邊");
}
</code>
end note
note left of org.study.rules.StarSectRule::finished
<code>
@Override
protected void finished(Description description) {
System.out.println("[測試後] 星宿老仙 法力無邊");
}
</code>
end note
note left of org.junit.rules.TestWatcher::starting
<code>
/**
* Invoked when a test is about to start
*/
end note
note left of org.junit.rules.TestWatcher::finished
<code>
/**
* Invoked when a test method finishes (whether passing or failing)
*/
</code>
end note
@enduml
画 "项目中出现的类的简要类图" 一图所用到的代码
@startuml
title 项目中出现的类的简要类图
class org.example.SimpleAdder {
+ int add(int a, int b)
}
interface org.junit.rules.TestRule {
+ Statement apply(Statement base, Description description)
}
abstract org.junit.rules.TestWatcher
org.junit.rules.TestRule <|.. org.junit.rules.TestWatcher
org.junit.rules.TestWatcher <|-- org.study.rules.StarSectRule
abstract class org.junit.rules.TestWatcher {
+ Statement apply(final Statement base, final Description description)
- void succeededQuietly(Description description, List<Throwable> errors)
- void failedQuietly(Throwable e, Description description, List<Throwable> errors)
- void skippedQuietly(org.junit.internal.AssumptionViolatedException e, Description description, List<Throwable> errors)
- void startingQuietly(Description description, List<Throwable> errors)
- void finishedQuietly(Description description, List<Throwable> errors)
# void succeeded(Description description)
# void failed(Throwable e, Description description)
# void skipped(AssumptionViolatedException e, Description description)
# void skipped(org.junit.internal.AssumptionViolatedException e, Description description)
# void starting(Description description)
# void finished(Description description)
}
class org.study.rules.StarSectRule {
# void starting(Description description)
# void finished(Description description)
}
class org.study.SimpleAdderTest {
- final SimpleAdder adder
+ TestRule buildTestRule()
+ void testAdd()
+ {static} void main(String[] args)
}
note right of org.example.SimpleAdder::add
<code>
public int add(int a, int b) {
return a + b;
}
</code>
end note
note right of org.study.rules.StarSectRule::starting
<code>
@Override
protected void starting(Description description) {
System.out.println("[測試前] 星宿老仙 法力無邊");
}
</code>
end note
note right of org.study.rules.StarSectRule::finished
<code>
@Override
protected void finished(Description description) {
System.out.println("[測試後] 星宿老仙 法力無邊");
}
</code>
end note
@enduml