1. Overview
1.概述
In this article, we’re going to take a look at the extension model in the JUnit 5 testing library. As the name suggests, the purpose of Junit 5 extensions is to extend the behavior of test classes or methods, and these can be reused for multiple tests.
在这篇文章中,我们要看一下JUnit 5测试库中的扩展模型。顾名思义,Junit 5扩展的目的是扩展测试类或方法的行为,这些可以在多个测试中重复使用。
Before Junit 5, the JUnit 4 version of the library used two types of components for extending a test: test runners and rules. By comparison, JUnit 5 simplifies the extension mechanism by introducing a single concept: the Extension API.
在Junit 5之前,JUnit 4版本的库使用两种类型的组件来扩展一个测试:测试运行器和规则。相比之下,JUnit 5通过引入一个概念简化了扩展机制:Extension API。
2. JUnit 5 Extension Model
2.JUnit 5扩展模型
JUnit 5 extensions are related to a certain event in the execution of a test, referred to as an extension point. When a certain life cycle phase is reached, the JUnit engine calls registered extensions.
JUnit 5扩展与测试执行中的某个事件有关,被称为扩展点。当达到某个生命周期阶段时,JUnit引擎会调用注册的扩展。
Five main types of extension points can be used:
可以使用五种主要类型的扩展点。
- test instance post-processing
- conditional test execution
- life-cycle callbacks
- parameter resolution
- exception handling
We’ll go through each of these in more detail in the following sections.
我们将在下面的章节中更详细地介绍每一项内容。
3. Maven Dependencies
3.Maven的依赖性
First, let’s add the project dependencies we will need for our examples. The main JUnit 5 library we’ll need is junit-jupiter-engine:
首先,让我们添加我们的例子所需的项目依赖。我们需要的主要JUnit 5库是junit-jupiter-engine。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
Also, let’s also add two helper libraries to use for our examples:
另外,让我们也添加两个辅助库,以便在我们的例子中使用。
<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>
The latest versions of junit-jupiter-engine, h2 and log4j-core can be downloaded from Maven Central.
junit-jupiter-engine、h2和log4j-core的最新版本可以从Maven中心下载。
4. Creating JUnit 5 Extensions
4.创建JUnit 5扩展
To create a JUnit 5 extension, we need to define a class which implements one or more interfaces corresponding to the JUnit 5 extension points. All of these interfaces extend the main Extension interface, which is only a marker interface.
为了创建一个JUnit 5扩展,我们需要定义一个类,该类实现了一个或多个与JUnit 5扩展点相对应的接口。所有这些接口都扩展了主Extension接口,它只是一个标记接口。
4.1. TestInstancePostProcessor Extension
4.1.TestInstancePostProcessor 扩展
This type of extension is executed after an instance of a test has been created. The interface to implement is TestInstancePostProcessor which has a postProcessTestInstance() method to override.
这种类型的扩展是在一个测试实例被创建后执行的。要实现的接口是TestInstancePostProcessor,它有一个postProcessTestInstance()方法可以重写。
A typical use case for this extension is injecting dependencies into the instance. For example, let’s create an extension which instantiates a logger object, then calls the setLogger() method on the test instance:
这个扩展的一个典型用例是将依赖关系注入到实例中。例如,让我们创建一个扩展,它实例化了一个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);
}
}
As can be seen above, the postProcessTestInstance() method provides access to the test instance and calls the setLogger() method of the test class using the mechanism of reflection.
从上面可以看出,postProcessTestInstance()方法提供了对测试实例的访问,并使用反射机制调用测试类的setLogger()方法。
4.2. Conditional Test Execution
4.2.有条件的测试执行
JUnit 5 provides a type of extension that can control whether or not a test should be run. This is defined by implementing the ExecutionCondition interface.
JUnit 5提供了一种扩展,可以控制是否应该运行测试。这是由实现ExecutionCondition接口定义的。
Let’s create an EnvironmentExtension class which implements this interface and overrides the evaluateExecutionCondition() method.
让我们创建一个EnvironmentExtension类,它实现了这个接口并重写了evaluateExecutionCondition()方法。
The method verifies if a property representing the current environment name equals “qa” and disables the test in this case:
该方法验证一个代表当前环境名称的属性是否等于“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");
}
}
As a result, tests that register this extension will not be run on the “qa” environment.
因此,注册该扩展的测试将不会在“qa”环境下运行。
If we do not want a condition to be validated, we can deactivate it by setting the junit.conditions.deactivate configuration key to a pattern that matches the condition.
如果我们不希望一个条件被验证,我们可以通过设置 junit.conditions.deactivate配置键来停用它,以匹配该条件的模式。
This can be achieved by starting the JVM with the -Djunit.conditions.deactivate=<pattern> property, or by adding a configuration parameter to the LauncherDiscoveryRequest:
这可以通过使用-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. Lifecycle Callbacks
4.3.生命周期回调
This set of extensions is related to events in a test’s lifecycle and can be defined by implementing the following interfaces:
这套扩展与测试生命周期中的事件有关,可以通过实现以下接口来定义。
- BeforeAllCallback and AfterAllCallback – executed before and after all the test methods are executed
- BeforeEachCallBack and AfterEachCallback – executed before and after each test method
- BeforeTestExecutionCallback and AfterTestExecutionCallback – executed immediately before and immediately after a test method
If the test also defines its lifecycle methods, the order of execution is:
如果测试也定义了它的生命周期方法,那么执行的顺序是。
- BeforeAllCallback
- BeforeAll
- BeforeEachCallback
- BeforeEach
- BeforeTestExecutionCallback
- Test
- AfterTestExecutionCallback
- AfterEach
- AfterEachCallback
- AfterAll
- AfterAllCallback
For our example, let’s define a class which implements some of these interfaces and controls the behavior of a test that accesses a database using JDBC.
对于我们的例子,让我们定义一个类,它实现了其中的一些接口,并控制一个使用JDBC访问数据库的测试的行为。
First, let’s create a simple Employee entity:
首先,让我们创建一个简单的Employee实体。
public class Employee {
private long id;
private String firstName;
// constructors, getters, setters
}
We will also need a utility class that creates a Connection based on a .properties file:
我们还需要一个实用类,根据.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;
}
}
Finally, let’s add a simple JDBC-based DAO that manipulates Employee records:
最后,让我们添加一个简单的基于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
}
}
Let’s create our extension which implements some of the lifecycle interfaces:
让我们创建我们的扩展,实现一些生命周期接口:。
public class EmployeeDatabaseSetupExtension implements
BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
//...
}
Each of these interfaces contains a method we need to override.
这些接口中的每一个都包含一个我们需要覆盖的方法。
For the BeforeAllCallback interface, we will override the beforeAll() method and add the logic to create our employees table before any test method is executed:
对于BeforeAllCallback接口,我们将覆盖beforeAll()方法并添加逻辑,在执行任何测试方法之前创建我们的employees表。
private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();
@Override
public void beforeAll(ExtensionContext context) throws SQLException {
employeeDao.createTable();
}
Next, we will make use of the BeforeEachCallback and AfterEachCallback to wrap each test method in a transaction. The purpose of this is to roll back any changes to the database executed in the test method so that the next test will run on a clean database.
接下来,我们将利用BeforeEachCallback和AfterEachCallback来将每个测试方法包裹在一个事务中。这样做的目的是为了回滚在测试方法中执行的对数据库的任何改变,以便下一个测试将在一个干净的数据库上运行。
In the beforeEach() method, we will create a save point to use for rolling back the state of the database to:
在beforeEach()方法中,我们将创建一个保存点,用于回滚数据库的状态。
private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;
@Override
public void beforeEach(ExtensionContext context) throws SQLException {
con.setAutoCommit(false);
savepoint = con.setSavepoint("before");
}
Then, in the afterEach() method, we’ll roll back the database changes made during the execution of a test method:
然后,在afterEach()方法中,我们将回滚测试方法执行过程中的数据库变化。
@Override
public void afterEach(ExtensionContext context) throws SQLException {
con.rollback(savepoint);
}
To close the connection, we’ll make use of the afterAll() method, executed after all the tests have finished:
为了关闭连接,我们将利用afterAll()方法,在所有测试结束后执行。
@Override
public void afterAll(ExtensionContext context) throws SQLException {
if (con != null) {
con.close();
}
}
4.4. Parameter Resolution
4.4.参数解析
If a test constructor or method receives a parameter, this must be resolved at runtime by a ParameterResolver.
如果一个测试构造函数或方法收到一个参数,这必须在运行时由ParameterResolver解决。
Let’s define our own custom ParameterResolver that resolves parameters of type EmployeeJdbcDao:
让我们定义自己的自定义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();
}
}
Our resolver implements the ParameterResolver interface and overrides the supportsParameter() and resolveParameter() methods. The first of these verify the type of the parameter, while the second defines the logic to obtain a parameter instance.
我们的解析器实现了ParameterResolver接口并重写了supportsParameter()和resolveParameter()方法。其中第一个方法验证了参数的类型,而第二个方法定义了获得参数实例的逻辑。
4.5. Exception Handling
4.5.异常处理
Last but not least, the TestExecutionExceptionHandler interface can be used to define the behavior of a test when encountering certain types of exceptions.
最后但并非最不重要的是,TestExecutionExceptionHandler接口可用于定义测试遇到某些类型的异常时的行为。
For example, we can create an extension which will log and ignore all exceptions of type FileNotFoundException, while rethrowing any other type:
例如,我们可以创建一个扩展,它将记录并忽略所有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. Registering Extensions
5.注册扩展程序
Now that we have defined our test extensions, we need to register them with a JUnit 5 test. To achieve this, we can make use of the @ExtendWith annotation.
现在我们已经定义了我们的测试扩展,我们需要在JUnit 5测试中注册它们。为了实现这一点,我们可以利用@ExtendWith注解。
The annotation can be added multiple time to a test, or receive a list of extensions as a parameter:
注释可以被多次添加到测试中,或者接收一个扩展列表作为参数。
@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;
}
}
We can see our test class has a constructor with an EmployeeJdbcDao parameter which will be resolved by extending the EmployeeDaoParameterResolver extension.
我们可以看到我们的测试类有一个带有EmployeeJdbcDao参数的构造函数,它将通过扩展EmployeeDaoParameterResolver扩展来解决。
By adding the EnvironmentExtension, our test will only be executed in an environment different than “qa”.
通过添加EnvironmentExtension,我们的测试将只在不同于“qa”的环境中执行。
Our test will also have the employees table created and each method wrapped in a transaction by adding the EmployeeDatabaseSetupExtension. Even if the whenAddEmployee_thenGetEmploee() test is executed first, which adds one record to the table, the second test will find 0 records in the table.
我们的测试还将创建employees表,并通过添加EmployeeDatabaseSetupExtension将每个方法包裹在一个事务中。即使先执行whenAddEmployee_thenGetEmploee()测试,向表中添加一条记录,第二个测试也会在表中找到0条记录。
A logger instance will be added to our class by using the LoggingExtension.
通过使用LoggingExtension,一个记录器实例将被添加到我们的类。
Finally, our test class will ignore all FileNotFoundException instances, since it is adding the corresponding extension.
最后,我们的测试类将忽略所有FileNotFoundException实例,因为它正在添加相应的扩展。
5.1. Automatic Extension Registration
5.1.自动扩展注册
If we want to register an extension for all tests in our application, we can do so by adding the fully qualified name to the /META-INF/services/org.junit.jupiter.api.extension.Extension file:
如果我们想为我们的应用程序中的所有测试注册一个扩展,我们可以通过在/META-INF/services/org.junit.jupiter.api.extension.Extension文件中添加完全合格的名称来实现。
com.baeldung.extensions.LoggingExtension
For this mechanism to be enabled, we also need to set the junit.jupiter.extensions.autodetection.enabled configuration key to true. This can be done by starting the JVM with the –Djunit.jupiter.extensions.autodetection.enabled=true property, or by adding a configuration parameter to LauncherDiscoveryRequest:
为了启用这一机制,我们还需要将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. Programmatic Extension Registration
5.2.程序性扩展注册
Although registering extensions using annotations is a more declarative and unobtrusive approach, it has a significant disadvantage: we can’t easily customize the extension behavior. For example, with the current extension registration model, we can’t accept the database connection properties from the client.
虽然使用注解注册扩展是一种更加声明性和不引人注目的方法,但它有一个显著的缺点。我们不能轻易地定制扩展的行为。例如,在当前的扩展注册模式下,我们不能接受来自客户端的数据库连接属性。
In addition to the declarative annotation-based approach, JUnit provides an API to register extensions programmatically. For example, we can retrofit the JdbcConnectionUtil class to accept the connection properties:
除了基于声明性注解的方法外,JUnit 还提供了一个 API 来以编程方式注册扩展 p。例如,我们可以改造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;
}
}
Also, we should add a new constructor for the EmployeeDatabaseSetupExtension extension to support customized database properties:
另外,我们应该为EmployeeDatabaseSetupExtension扩展添加一个新的构造函数,以支持自定义的数据库属性。
public EmployeeDatabaseSetupExtension(String url, String driver, String username, String password) {
con = JdbcConnectionUtil.getConnection(url, driver, username, password);
employeeDao = new EmployeeJdbcDao(con);
}
Now, to register the employee extension with custom database properties, we should annotate a static field with the @RegisterExtension annotation:
现在,为了用自定义数据库属性注册雇员扩展,我们应该用@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
}
Here, we’re connecting to an in-memory H2 database to run the tests.
在这里,我们连接到一个内存中的H2数据库来运行测试。
5.3. Registration Ordering
5.3.登记订购
JUnit registers @RegisterExtension static fields after registering extensions that are declaratively defined using the @ExtendsWith annotation. We can also use non-static fields for programmatic registration, but they will be registered after the test method instantiation and post processors.
JUnit在注册使用@RegisterExtension注解声明性定义的扩展后,会注册@ExtendsWith静态字段。我们也可以使用非静态字段进行程序化注册,但它们将在测试方法实例化和后处理程序后被注册。
If we register multiple extensions programmatically, via @RegisterExtension, JUnit will register those extensions in a deterministic order. Although the ordering is deterministic, the algorithm used for the ordering is non-obvious and internal. To enforce a particular registration ordering, we can use the @Order annotation:
如果我们通过@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
}
Here, extensions are ordered based on priority, where a lower value has greater priority than a higher value. Also, extensions with no @Order annotation would have the lowest possible priority.
在这里,扩展被根据优先级排序,低值比高值有更大的优先级。另外,没有@Order 注释的扩展将具有最低的优先级。
6. Conclusion
6.结论
In this tutorial, we have shown how we can make use of the JUnit 5 extension model to create custom test extensions.
在本教程中,我们展示了如何利用JUnit 5扩展模型来创建自定义测试扩展。
The full source code of the examples can be found over on GitHub.
示例的完整源代码可以在GitHub上找到over。