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. 生命周期回调
此类扩展与测试生命周期事件相关,通过实现以下接口定义:
- BeforeAllCallback 和 AfterAllCallback – 所有测试方法执行前后运行
- BeforeEachCallBack 和 AfterEachCallback – 每个测试方法执行前后运行
- BeforeTestExecutionCallback 和 AfterTestExecutionCallback – 测试方法执行前后立即运行
若测试类也定义了生命周期方法,执行顺序为:
- BeforeAllCallback
- BeforeAll
- BeforeEachCallback
- BeforeEach
- BeforeTestExecutionCallback
- Test
- AfterTestExecutionCallback
- AfterEach
- AfterEachCallback
- AfterAll
- 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(); }
BeforeEachCallback 和 AfterEachCallback:为每个测试方法包装事务,确保测试间数据库状态隔离
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. 注册顺序
⚠️ 注册顺序规则:
- @ExtendWith 声明的扩展优先注册
- @RegisterExtension 静态字段次之
- 非 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 仓库。