1. 概述

本文将深入探讨 JUnit 5 测试库中的扩展模型。顾名思义,JUnit 5 扩展的核心目的是增强测试类或方法的行为,且这些扩展可在多个测试中复用。

在 JUnit 5 之前,JUnit 4 使用两种组件实现扩展功能:测试运行器(Test Runners)和规则(Rules)。相比之下,JUnit 5 通过引入单一概念——Extension API——大幅简化了扩展机制。

2. JUnit 5 扩展模型

JUnit 5 扩展与测试执行过程中的特定事件(称为扩展点)相关联。当测试生命周期到达特定阶段时,JUnit 引擎会调用已注册的扩展。

主要有五种扩展点类型:

  • 测试实例后处理(Test Instance Post-Processing)
  • 条件测试执行(Conditional Test Execution)
  • 生命周期回调(Lifecycle Callbacks)
  • 参数解析(Parameter Resolution)
  • 异常处理(Exception Handling)

后续章节将逐一详细说明这些类型。

3. Maven 依赖

首先,为示例项目添加必要依赖。核心依赖是 junit-jupiter-engine

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

同时添加两个辅助库用于示例:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
</dependency>

最新版本可从 Maven Central 下载:

4. 创建 JUnit 5 扩展

创建 JUnit 5 扩展需定义一个类,实现一个或多个与扩展点对应的接口。所有这些接口均继承自仅作标记用的主接口 Extension

4.1. TestInstancePostProcessor 扩展

此类扩展在测试实例创建后执行。需实现 TestInstancePostProcessor 接口并重写 postProcessTestInstance() 方法。

典型用例是向测试实例注入依赖。例如,创建一个扩展来实例化 logger 对象,并通过反射调用测试类的 setLogger() 方法:

public class LoggingExtension implements TestInstancePostProcessor {

    @Override
    public void postProcessTestInstance(Object testInstance, 
      ExtensionContext context) throws Exception {
        Logger logger = LogManager.getLogger(testInstance.getClass());
        testInstance.getClass()
          .getMethod("setLogger", Logger.class)
          .invoke(testInstance, logger);
    }
}

如上所示,postProcessTestInstance() 方法通过反射机制访问测试实例并调用其 setLogger() 方法。

4.2. 条件测试执行

JUnit 5 提供控制测试是否运行的扩展类型,需实现 ExecutionCondition 接口。

创建 EnvironmentExtension 类实现该接口,重写 evaluateExecutionCondition() 方法。该方法检查当前环境名属性是否为 *"qa"*,若是则禁用测试:

public class EnvironmentExtension implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(
      ExtensionContext context) {
        
        Properties props = new Properties();
        props.load(EnvironmentExtension.class
          .getResourceAsStream("application.properties"));
        String env = props.getProperty("env");
        if ("qa".equalsIgnoreCase(env)) {
            return ConditionEvaluationResult
              .disabled("Test disabled on QA environment");
        }
        
        return ConditionEvaluationResult.enabled(
          "Test enabled on QA environment");
    }
}

注册此扩展的测试在 "qa" 环境下将不会执行。

⚠️ 若需禁用某个条件,可通过设置 junit.conditions.deactivate 配置键实现。可通过 JVM 参数 -Djunit.conditions.deactivate=<pattern> 或在 LauncherDiscoveryRequest 中添加配置参数:

public class TestLauncher {
    public static void main(String[] args) {
        LauncherDiscoveryRequest request
          = LauncherDiscoveryRequestBuilder.request()
          .selectors(selectClass("com.baeldung.EmployeesTest"))
          .configurationParameter(
            "junit.conditions.deactivate", 
            "com.baeldung.extensions.*")
          .build();

        TestPlan plan = LauncherFactory.create().discover(request);
        Launcher launcher = LauncherFactory.create();
        SummaryGeneratingListener summaryGeneratingListener
          = new SummaryGeneratingListener();
        launcher.execute(
          request, 
          new TestExecutionListener[] { summaryGeneratingListener });
 
        System.out.println(summaryGeneratingListener.getSummary());
    }
}

4.3. 生命周期回调

此类扩展与测试生命周期事件相关,通过实现以下接口定义:

  • BeforeAllCallbackAfterAllCallback – 所有测试方法执行前后运行
  • BeforeEachCallBackAfterEachCallback – 每个测试方法执行前后运行
  • BeforeTestExecutionCallbackAfterTestExecutionCallback – 测试方法执行前后立即运行

若测试类也定义了生命周期方法,执行顺序为:

  1. BeforeAllCallback
  2. BeforeAll
  3. BeforeEachCallback
  4. BeforeEach
  5. BeforeTestExecutionCallback
  6. Test
  7. AfterTestExecutionCallback
  8. AfterEach
  9. AfterEachCallback
  10. AfterAll
  11. AfterAllCallback

以数据库测试为例,创建控制 JDBC 访问的扩展。首先定义简单实体类 Employee

public class Employee {

    private long id;
    private String firstName;
    // 构造函数、getter、setter
}

创建基于 .properties 文件的 Connection 工具类:

public class JdbcConnectionUtil {

    private static Connection con;

    public static Connection getConnection() 
      throws IOException, ClassNotFoundException, SQLException{
        if (con == null) {
            // 创建连接
            return con;
        }
        return con;
    }
}

添加操作 Employee 记录的 JDBC DAO:

public class EmployeeJdbcDao {
    private Connection con;

    public EmployeeJdbcDao(Connection con) {
        this.con = con;
    }

    public void createTable() throws SQLException {
        // 创建 employees 表
    }

    public void add(Employee emp) throws SQLException {
       // 添加员工记录
    }

    public List<Employee> findAll() throws SQLException {
       // 查询所有员工记录
    }
}

创建实现多个生命周期接口的扩展:

public class EmployeeDatabaseSetupExtension implements 
  BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
    //...
}

各接口需重写对应方法:

  • BeforeAllCallback:在所有测试前创建 employees

    private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();
    
    @Override
    public void beforeAll(ExtensionContext context) throws SQLException {
        employeeDao.createTable();
    }
    
  • BeforeEachCallbackAfterEachCallback:为每个测试方法包装事务,确保测试间数据库状态隔离

    private Connection con = JdbcConnectionUtil.getConnection();
    private Savepoint savepoint;
    
    @Override
    public void beforeEach(ExtensionContext context) throws SQLException {
        con.setAutoCommit(false);
        savepoint = con.setSavepoint("before");
    }
    
    @Override
    public void afterEach(ExtensionContext context) throws SQLException {
        con.rollback(savepoint);
    }
    
  • AfterAllCallback:测试结束后关闭连接

    @Override
    public void afterAll(ExtensionContext context) throws SQLException {
        if (con != null) {
            con.close();
        }
    }
    

4.4. 参数解析

当测试构造器或方法含参数时,需通过 ParameterResolver 在运行时解析。

创建解析 EmployeeJdbcDao 类型参数的自定义解析器:

public class EmployeeDaoParameterResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, 
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.getParameter().getType()
          .equals(EmployeeJdbcDao.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, 
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return new EmployeeJdbcDao();
    }
}

解析器实现 ParameterResolver 接口,重写:

  • *supportsParameter()*:验证参数类型
  • *resolveParameter()*:定义获取参数实例的逻辑

4.5. 异常处理

TestExecutionExceptionHandler 接口用于定义测试遇到特定异常时的行为。

例如,创建扩展记录并忽略 FileNotFoundException,其他异常则重新抛出:

public class IgnoreFileNotFoundExceptionExtension 
  implements TestExecutionExceptionHandler {

    Logger logger = LogManager
      .getLogger(IgnoreFileNotFoundExceptionExtension.class);
    
    @Override
    public void handleTestExecutionException(ExtensionContext context,
      Throwable throwable) throws Throwable {

        if (throwable instanceof FileNotFoundException) {
            logger.error("File not found:" + throwable.getMessage());
            return;
        }
        throw throwable;
    }
}

5. 注册扩展

定义好扩展后,需将其注册到 JUnit 5 测试中。使用 @ExtendWith 注解实现:

@ExtendWith({ EnvironmentExtension.class, 
  EmployeeDatabaseSetupExtension.class, EmployeeDaoParameterResolver.class })
@ExtendWith(LoggingExtension.class)
@ExtendWith(IgnoreFileNotFoundExceptionExtension.class)
public class EmployeesTest {
    private EmployeeJdbcDao employeeDao;
    private Logger logger;

    public EmployeesTest(EmployeeJdbcDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    @Test
    public void whenAddEmployee_thenGetEmployee() throws SQLException {
        Employee emp = new Employee(1, "john");
        employeeDao.add(emp);
        assertEquals(1, employeeDao.findAll().size());   
    }
    
    @Test
    public void whenGetEmployees_thenEmptyList() throws SQLException {
        assertEquals(0, employeeDao.findAll().size());   
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

关键点:

  • ✅ 构造器中的 EmployeeJdbcDao 参数由 EmployeeDaoParameterResolver 解析
  • EnvironmentExtension 确保测试仅在非 "qa" 环境执行
  • EmployeeDatabaseSetupExtension 创建表并为每个方法包装事务(即使第一个测试插入数据,第二个测试仍查到空表)
  • LoggingExtension 注入 logger 实例
  • IgnoreFileNotFoundExceptionExtension 忽略文件未找到异常

5.1. 自动扩展注册

若需为应用中所有测试注册扩展,在 /META-INF/services/org.junit.jupiter.api.extension.Extension 文件中添加全限定名:

com.baeldung.extensions.LoggingExtension

启用此机制需设置 junit.jupiter.extensions.autodetection.enabled 为 true:

  • JVM 参数:-Djunit.jupiter.extensions.autodetection.enabled=true
  • 编程方式:
    LauncherDiscoveryRequest request
      = LauncherDiscoveryRequestBuilder.request()
      .selectors(selectClass("com.baeldung.EmployeesTest"))
      .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true")
    .build();
    

5.2. 编程式扩展注册

注解方式虽声明性强,但无法灵活定制扩展行为(如动态传递数据库配置)。JUnit 提供编程式注册 API。

改造 JdbcConnectionUtil 支持自定义连接属性:

public class JdbcConnectionUtil {

    private static Connection con;

    // 原无参 getConnection

    public static Connection getConnection(String url, String driver, String username, String password) {
        if (con == null) {
            // 创建连接 
            return con;
        }
        return con;
    }
}

EmployeeDatabaseSetupExtension 添加新构造器:

public EmployeeDatabaseSetupExtension(String url, String driver, String username, String password) {
    con = JdbcConnectionUtil.getConnection(url, driver, username, password);
    employeeDao = new EmployeeJdbcDao(con);
}

使用 @RegisterExtension 注解注册带自定义配置的扩展:

@ExtendWith({EnvironmentExtension.class, EmployeeDaoParameterResolver.class})
public class ProgrammaticEmployeesUnitTest {

    private EmployeeJdbcDao employeeDao;

    @RegisterExtension 
    static EmployeeDatabaseSetupExtension DB =
      new EmployeeDatabaseSetupExtension("jdbc:h2:mem:AnotherDb;DB_CLOSE_DELAY=-1", "org.h2.Driver", "sa", "");

    // 构造器和测试方法同前
}

此示例连接内存 H2 数据库运行测试。

5.3. 注册顺序

⚠️ 注册顺序规则

  1. @ExtendWith 声明的扩展优先注册
  2. @RegisterExtension 静态字段次之
  3. 非 static 字段最后注册(在测试实例化后)

多个 @RegisterExtension 扩展的注册顺序默认不确定。强制指定顺序需使用 @Order 注解

public class MultipleExtensionsUnitTest {

    @Order(1) 
    @RegisterExtension 
    static EmployeeDatabaseSetupExtension SECOND_DB = // ...

    @Order(0)
    @RegisterExtension     
    static EmployeeDatabaseSetupExtension FIRST_DB = // ...

    @RegisterExtension     
    static EmployeeDatabaseSetupExtension LAST_DB = // ...

    // ...
}
  • ✅ 优先级规则:数值越小优先级越高
  • ✅ 未标注 @Order 的扩展优先级最低

6. 总结

本文系统讲解了如何利用 JUnit 5 扩展模型创建自定义测试扩展。通过扩展点机制,可灵活实现测试增强、条件控制、资源管理等功能。

完整示例代码见 GitHub 仓库


原始标题:A Guide to JUnit 5 Extensions