浅解 JUnit 4 第十四篇:如何实现一个 @After 注解的替代品?

背景

在以下两篇文章中,我们探讨了 如何实现一个 @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 注解时,我们预期的行为是什么呢?下方的思维导图里举了一个例子 ⬇️

所以我们的目标是是

  1. 实现一个类似 @After 的注解(例如可以叫 @MyAfter
  2. 在运行每一个测试方法后,让带有 @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 注解的代码 ⬇️ (MyAfterRuleMethodRule 接口的一个实现类)

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 SimpleAdderTestmain 方法。当程序运行到 断点甲 这里时,可以观察到

  • method 变量和 SimpleAdderTest 类里的 testAdd() 方法对应
  • myAfterMethods 变量和 SimpleAdderTest 类里所有带有 @MyAfter 注解的方法对应(SimpleAdderTest 类里只有 postRun() 方法带有 @MyAfter 注解)

其他

PlantUML 画图,所用到的代码

画 "MethodRuleTestRule 的简要类图" 一图所用到的代码

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
相关推荐
金銀銅鐵2 小时前
浅解 JUnit 4 第十三篇:如何实现一个 @Before 注解的替代品?(下)
junit·单元测试
金銀銅鐵3 天前
浅解 JUnit 4 第十二篇:如何生成 @Before 注解的替代品?(上)
junit·单元测试
Apifox4 天前
【测试套件】当用户说“我只想跑 P0 用例”时,我们到底在说什么
单元测试·测试·ab测试
金銀銅鐵7 天前
浅解 JUnit 4 第十一篇:@Before 注解和 @After 注解如何发挥作用?
junit·单元测试
金銀銅鐵8 天前
浅解 JUnit 4 第十篇:方法上的 @Ignore 注解
junit·单元测试
阿狸猿10 天前
单元测试中静态测试、动态测试及白盒测试、回归测试实践
单元测试·软考
Max_uuc10 天前
【工程心法】从“在板盲调”到“云端验证”:嵌入式单元测试与 TDD 的工程化革命
单元测试·tdd
feathered-feathered11 天前
测试实战【用例设计】自己写的项目+功能测试(1)
java·服务器·后端·功能测试·jmeter·单元测试·压力测试