JUnit 5 扩展指南
上次更新:2023 年 2 月 23 日
1. 概述
在本文中,我们将深入了解 JUnit 5 测试库中的扩展模型。顾名思义,JUnit 5 扩展的目的是扩展测试类或方法的行为,并且这些扩展可以在多个测试中重用。
在 JUnit 5 之前,JUnit 4 版本的库使用了两种类型的组件来扩展测试:测试运行器和规则。相比之下,JUnit 5 通过引入一个单一的概念来简化扩展机制:Extension API。
2. JUnit 5 扩展模型
JUnit 5 扩展与测试执行中的某个事件相关联,该事件被称为扩展点。当达到某个生命周期阶段时,JUnit 引擎会调用已注册的扩展。
可以使用五种主要的扩展点类型
- 测试实例后处理
- 条件测试执行
- 生命周期回调
- 参数解析
- 异常处理
我们将在后续章节中更详细地介绍这些内容。
3. Maven 依赖
首先,让我们添加示例所需的项目依赖项。我们需要的主要 JUnit 5 库是 junit-jupiter-engine
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.0-M2</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 下载最新版本的 junit-jupiter-engine、h2 和 log4j-core。
4. 创建 JUnit 5 扩展
要创建 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 配置键设置为与该条件匹配的模式来停用它。
可以通过使用 -Djunit.conditions.deactivate=<pattern> 属性启动 JVM,或通过向 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;
// constructors, getters, setters
}
我们还需要一个工具类,它基于 .properties 文件创建一个 Connection
public class JdbcConnectionUtil {
private static Connection con;
public static Connection getConnection()
throws IOException, ClassNotFoundException, SQLException{
if (con == null) {
// create connection
return con;
}
return con;
}
}
最后,让我们添加一个简单的基于 JDBC 的 DAO,它操作 Employee 记录
public class EmployeeJdbcDao {
private Connection con;
public EmployeeJdbcDao(Connection con) {
this.con = con;
}
public void createTable() throws SQLException {
// create employees table
}
public void add(Employee emp) throws SQLException {
// add employee record
}
public List<Employee> findAll() throws SQLException {
// query all employee records
}
}
让我们创建我们的扩展,它实现一些生命周期接口
public class EmployeeDatabaseSetupExtension implements
BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
//...
}
这些接口中的每一个都包含我们需要重写的一个方法。
对于 BeforeAllCallback 接口,我们将重写 beforeAll() 方法,并在执行任何测试方法之前添加创建我们的 employees 表的逻辑
private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();
@Override
public void beforeAll(ExtensionContext context) throws SQLException {
employeeDao.createTable();
}
接下来,我们将利用 BeforeEachCallback 和 AfterEachCallback 将每个测试方法包装在事务中。 这样做是为了回滚测试方法中执行的任何对数据库的更改,以便下一个测试将在干净的数据库上运行。
在 beforeEach() 方法中,我们将创建一个保存点,用于将数据库的状态回滚到
private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;
@Override
public void beforeEach(ExtensionContext context) throws SQLException {
con.setAutoCommit(false);
savepoint = con.setSavepoint("before");
}
然后在 afterEach() 方法中,我们将回滚在测试方法执行期间所做的数据库更改
@Override
public void afterEach(ExtensionContext context) throws SQLException {
con.rollback(savepoint);
}
为了关闭连接,我们将利用 afterAll() 方法,该方法在所有测试完成之后执行
@Override
public void afterAll(ExtensionContext context) throws SQLException {
if (con != null) {
con.close();
}
}
4.4. 参数解析
如果测试构造函数或方法接收一个参数,则必须在运行时由 ParameterResolver 解析该参数。
让我们定义我们自己的自定义 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,我们的测试还将创建employees表,并且每个方法都将包含在一个事务中。即使先执行whenAddEmployee_thenGetEmploee()测试,该测试会将一条记录添加到表中,第二次测试也会发现表中 0 条记录。
通过使用LoggingExtension,将添加一个日志器实例到我们的类中。
最后,我们的测试类将忽略所有FileNotFoundException实例,因为它正在添加相应的扩展。
5.1. 自动扩展注册
如果我们要为应用程序中的所有测试注册一个扩展,可以通过将完全限定名称添加到/META-INF/services/org.junit.jupiter.api.extension.Extension文件中来实现。
com.baeldung.extensions.LoggingExtension
要启用此机制,我们还需要将junit.jupiter.extensions.autodetection.enabled配置键设置为 true。可以通过使用 –Djunit.jupiter.extensions.autodetection.enabled=true属性启动 JVM,或者通过向LauncherDiscoveryRequest添加配置参数来完成此操作。
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;
// no-arg getConnection
public static Connection getConnection(String url, String driver, String username, String password) {
if (con == null) {
// create connection
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", "");
// same constrcutor and tests as before
}
这里,我们正在连接到内存中的 H2 数据库来运行测试。
5.3. 注册顺序
JUnit会在使用@ExtendsWith注释声明性定义扩展后,注册@RegisterExtension 静态字段。我们也可以使用非静态字段进行程序化注册,但它们将在测试方法实例化和后处理器之后注册。
如果我们通过@RegisterExtension程序化地注册多个扩展,JUnit将以确定性的顺序注册这些扩展。虽然顺序是确定的,但用于排序的算法并不明显且是内部的。为了强制特定的注册顺序,我们可以使用 @Order 注释:
public class MultipleExtensionsUnitTest {
@Order(1)
@RegisterExtension
static EmployeeDatabaseSetupExtension SECOND_DB = // omitted
@Order(0)
@RegisterExtension
static EmployeeDatabaseSetupExtension FIRST_DB = // omitted
@RegisterExtension
static EmployeeDatabaseSetupExtension LAST_DB = // omitted
// omitted
}
这里,扩展是根据优先级排序的,其中较低的值比较高的值具有更高的优先级。 此外,没有 @Order 注释的扩展将具有最低的优先级。
6. 结论
在本教程中,我们展示了如何利用 JUnit 5 扩展模型来创建自定义测试扩展。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















