浅解 JUnit 4 第十六篇:TestWatcher

1 阅读6分钟

背景

浅解 JUnit 4 第十五篇:如何在测试方法运行前后做些事情? 一文中,我们探讨了 如何在测试方法运行前后做些事情 的问题。可以这样概括 ⬇️

  • TestRule\text{TestRule} 接口的实现类中,可以自由控制测试方法运行前后要执行的逻辑。
  • 通过 override TestRule\text{TestRule} 接口的 apply(Statement base, Description description) 方法,可以将 statementbasestatement_{base} 包装为 statementreturnstatement_{return}statementreturnstatement_{return} 里的 evaluate() 方法决定何时调用 statementbasestatement_baseevaluate() 方法

在此基础上,我们可以想到,有不少场景都需要“观察(而非影响)测试方法的运行”的行为,例如

  • 在测试方法运行前后打印固定的日志
  • 统计测试方法运行的耗时
  • 在测试方法运行前后分配和释放资源

既然这样的场景很常见,那么在这些场景中,JUnit 4 中能否提供比 TestRule 更好用的类/接口呢?答案是肯定的。org.junit.rules.TestWatcher\text{org.junit.rules.TestWatcher} 就可以用于这些场景。

本文的主角是 org.junit.rules.TestWatcher\text{org.junit.rules.TestWatcher}

要点

  • Statement\text{Statement} 可以封装“运行测试方法”的逻辑,通过使用层层包装的 Statement\text{Statement} 对象,我们可以实现各种对测试方法的 前置/后置/前置+后置 处理

  • 通过实现 TestRule\text{TestRule} 接口(或者实现 MethodRule\text{MethodRule} 接口),我们就可以自由控制测试方法 执行前/执行后/执行前+执行后 的逻辑

  • StarSectRule\text{StarSectRule} (这个类是我自己写的)作为 TestWatcher\text{TestWatcher} 的子类,间接实现了 TestRule\text{TestRule} 接口,TestWatcher\text{TestWatcher} 会负责在测试方法运行的各个时机(例如 开始前/成功后/跳过后/失败后/结束时)调用相应的方法,由于本文的例子里只关心 开始前/结束时 这两个时机,我们只需要 override 对应的方法(即 starting(Description description) 方法和 finished(Description description) 方法),就能做到如下的效果 ⬇️

    • 首先 输出星宿派的一个口号
    • 其次 调用 testAdd() 方法
    • 最后 输出星宿派的一个口号

c.png

正文

org.junit.rules.TestWatcher\text{org.junit.rules.TestWatcher}javadoc

我们先看看 org.junit.rules.TestWatcher\text{org.junit.rules.TestWatcher}javadoc ⬇️

image.png

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).

第三部分建议总是将 TestWatcher\text{TestWatcher}order 设置为 Integer.MIN_VALUE,这样就能保证它能观测到最外层的"成功"/"失败"(因为内层的 Rule 可以将"成功"调整为"失败",或者将"失败"调整为"成功")。

order@Rule 注解中的一个方法 ⬇️

image.png

一个具体的例子

浅解 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>

项目中出现的类的简要类图如下 ⬇️

b.png

运行结果

运行 SimpleAdderTest 中的 main 方法,应该能看到下图所示的效果 ⬇️

image.png

其中 红色框 的内容和 testAdd() 方法有关,黄色框 的内容和 前置/后置 处理有关。

简单的分析

如果想探索一下 StarSectRule\text{StarSectRule} 是如何起作用的,那么可以在 StarSectRule\text{StarSectRule} 里打一个断点(断点的位置如下图所示)

image.png

为了便于描述,我们将这个断点简称为 断点甲

我们 debug SimpleAdderTestmain 方法,当程序运行到 断点甲 这里时,可以看到如下的函数调用栈 ⬇️

image.png

可见, TestWatcher 使用了模版方法这种设计模式(即基类中定好整体的流程,但是允许子类 override 其中的某些方法)。更多细节,这里就不展开了,各位可以自己打断点研究。核心的点还是以下几个

  • 通过 TestClassT\text{TestClass}_\text{T} 可以查到测试类 T\text{T} 中哪些方法/字段带有指定的注解(例如 @Test/@Before/@After/@Rule/@Ignore)
  • 通过各种 Runner\text{Runner} 可以封装“运行测试”的逻辑
  • 通过各种 RunnerBuilder\text{RunnerBuilder} 可以构建相应的 Runner\text{Runner}
  • 通过 Statement\text{Statement} 可以封装“运行一个测试方法”的逻辑
  • 通过使用层层包装的 Statement\text{Statement} 对象,可以自由控制运行测试方法前后要做什么
  • TestRule\text{TestRule} 接口中定义了 apply(Statement base, Description description) 方法,我们可以在它的实现类里完成 Statement\text{Statement} 的 前置/后置/前置+后置 处理逻辑

其他

用 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

参考资料