JUnit 5 Extensions : A Detailed Guide

Ghislain Kalonji Mukendi

Posted On: August 3, 2021

view count244169 Views

Read time20 Min Read




JUnit is one of the most popular unit testing frameworks in the Java ecosystem. The JUnit 5 version (also known as Jupiter) contains many exciting innovations, including support for new features in Java 8 and above. However, many developers still prefer to use the JUnit 4 framework since certain features like parallel execution with JUnit 5 are still in the experimental phase.

Barring a few minor things aside, JUnit 5 still represents a major step forward in test framework evolution as it provides advanced annotations that let you test reactive applications. As per my experience, JUnit 5 is the best JUnit version yet. The new framework also brings in an extensible architecture and a brand-new extension model that makes it super easy to implement custom features.

JUnit 5 Extensions

Source

In this JUnit Tutorial, we deep dive into JUnit 5 extensions – one of the major features of the JUnit 5 framework. To delve deeper into the realm of JUnit, explore our dedicated hub focusing on Junit interview questions.

What’s so great about JUnit 5?

If you have used the JUnit 4 framework, you would agree that there are reduced (or minimal) possibilities of extending or customizing the JUnit 4 framework. This is one of the biggest bottlenecks in that version of the JUnit framework. In JUnit 4, extensions like Runners can be created by simply annotating the test class with @RunWith(MyRunner.class) so that JUnit can use them.

The downside of this approach is that you use only one Runner for a test class. This makes it difficult to compose with multiple runners. However, the shortcomings posed by Runners with JUnit 4 can be overcome using the below options:

  • JUnit 4 uses the Rules in addition to Runners that provides you a flexible solution to add or redefine the behavior of each test method.
  • Rules can be created to annotate fields of the test class. However, Rules suffers from a constancy problem. In simple terms, Rules can only be executed before and after a test is run but can’t be implemented within the test.

So, how does the JUnit 5 framework solve this lingering problem of JUnit 4? JUnit 5 offers an extension mechanism that opens third-party tools or APIs through the extension model. It consists of a single and coherent concept of Extension APIs to overcome the limitations of competing JUnit 4’s extension points (i.e., Runner, TestRule, and MethodRule).

Now that we have covered a gist about JUnit 5 Extensions, here are the immediate set of questions that pops up for Java developers:

  • Why should we use extensions?
  • How much effort is involved in coming with JUnit 5 extensions?
  • Is the extension model better than the “Programming Model”?

Here is what is mentioned in JUnit 5’s core principles:

It’s better to enable new functionality by creating or augmenting an extension point rather than adding the functionality as a core feature.

JUnit

JUnit 5 Architecture

The previous versions of the JUnit framework (i.e., till JUnit 4) were delivered in a single jar. However, JUnit 5 is architecturally different from the earlier JUnit versions. Therefore, JUnit 5 is delivered in different modules to meet the new architecture that separates API, Execution engine, Execution, and Integration.

JUnit 5 can only be used with Java versions greater than or equal to 8. Here are the three modules that make up the JUnit 5 framework:

  1. JUnit Platform: Provides an API for tools to discover and run tests. It defines an interface between JUnit and customers who want to run the tests from IDEs, build tools, or console.
  2. JUnit Jupiter: Provides an annotation-based API to write JUnit 5 unit tests, along with a test engine that lets you run them.
  3. JUnit Vintage: Offers a test engine to run JUnit 3 and JUnit 4 tests, thereby ensuring backward compatibility (with earlier versions of the JUnit framework).

The goal of this architecture is to separate the responsibilities of testing, execution, and extensions. It also facilitates the integration of other test frameworks with the JUnit framework.

JUnit 5 Architecture

Programming Model Vs. Extension Model

If you are a QA engineer who writes tests on a regular basis, you are sure to use the programming model. On the other hand, the Extension model provides several interfaces as extension APIs that can be implemented by extension providers (developers or tool vendors) to extend the core functionality of JUnit 5.

JUnit 5 Architecture

JUnit 5 Architecture

As seen in the JUnit 5 architecture shown above, the extension model is a part of the Jupiter module that lets you extend the core features of JUnit 5 through flexible and powerful extensions. In addition, the JUnit 5 extension overcomes the limitations of the JUnit 4 extension by replacing Runners and Rules, its competing extension mechanisms. Finally, since JUnit 5 provides backward compatibility, you can still run JUnit 4 tests with JUnit 5.

The extension model of JUnit Jupiter is exposed through a small interface in org.junit.jupiter.api.extension package that can be used by developers or extension providers.

Now that we have covered the essentials of JUnit 5 Extensions let’s get our hands dirty with code that illustrates a JUnit 5 extension example. For doing so, let’s create a Java project with three test cases in a Java class using the Eclipse IDE:

JUnit 5 Extensions

JUnit 5 Extensions

In case you are familiar with other Java IDE (apart from Eclipse), you can check our detailed blog that deep-dives into How to run JUnit from the Eclipse IDE. After adding the JUnit 5 library to the build path (or adding dependencies for the Maven project), we see that the JUnit 5 extension is in the org.junit.jupiter.api in the org.junit.jupiter.api.extension package as shown below:

Here is a sample Java implementation that showcases a simple JUnit 5 extension example:

As seen in the above implementation, we have used JUnit annotations related to the test execution lifecycle, which we will discuss at a later point in time.

LambdaTest has come up with free JUnit certification for Java developers that would help accelerate your career in Java development and testing. A short glimpse of the JUnit certification from LambdaTest:

How to register JUnit 5 extensions

Extension registration in JUnit 5 is done to register one or more extensions via Java’s ServiceLoader mechanism. There are three ways of registering extensions: Declaratively, Programmatically, and Automatically.

Registration of one or more extensions can be done using annotations on the test interface, test class (or its field), or test method depending on the type of registration:

  • Declarative registration: The @ExtendWith(classReference.class) annotation
    should be used for applying the extension to class fields, test interfaces, test methods, or custom composed annotations

    To demonstrate this using a JUnit 5 extension example, we have used a sample that shows the handling of test result exceptions:

    We have used @ExtendWith (AdditionalOutputExtension.class) annotation to register the above class so that the JUnit framework can use it at a later stage.

  • Programmatic registration: We can use the @RegisterExtension annotation by applying them to fields in test classes:
  • Automatic registration: We can use java.util.ServiceLoader to auto-detect and register third-party extensions.

JUnit 5 Conditional Test Execution With Annotations

For starters, conditional test execution allows test cases to be run (enabled) or skipped (disabled) based on certain conditions via the org.junit.jupiter.api.condition API. Let’s look at how annotations of the condition package can be used for realizing conditional test execution in JUnit 5.

1. Operating System Conditions

Operating system conditions can be used with @EnabledOnOs and @DisabledOnOs annotations. The conditions help in running the JUnit 5 test on a particular platform (or operating system).

2. Java Runtime Environment Conditions

Test cases can be run under certain conditions related to JRE (Java Runtime Environment) or on a certain range of the JRE version’s range using @EnabledOnJre, @DisabledOnJre, and @EnabledForJreRange annotations.

3. System Property Conditions

Test cases can be enabled or disabled based on the system property using the @EnabledIfSystemProperty and/or @DisabledIfSystemProperty annotations.

4. Environment Variable Conditions

JUnit 5 test cases can be enabled or disabled based on the condition (or value) of the environment variables. This can be done using @EnabledIfEnvironmentVariable and @DisabledIfEnvironmentVariable annotations in the JUnit 5 framework.

5. Custom Conditions

Custom conditions can be set to enable or disable test cases via the ExecutionCondition extension API. Here are the two ways through which you can implement test cases that run under particular (custom) conditions:

  • Combine built-in annotations to create a custom annotation that can be used later as a test condition.

    The combined built-in annotations inside the test condition can be used as an annotation inside a test class. It will help to define conditions under which the test should be performed.

  • Custom annotation can be created from scratch using the ExecutionCondition extension API. Using this approach, you can get around without using the built-in annotations. To demonstrate custom annotations using the JUnit 5 extension example, we run tests under the condition of the runtime environment (i.e., the environment could be development, QA, or production) as shown below:

    Here, the condition to run the add () test is executed in the test or development environment (not live). Here is how you can create the @Environment annotation from scratch and implement it in the JUnit 5 extension example:

    • We create an Environment.java file and set the enabledFor attribute to add parameters on it. Next, the created annotation must register the condition extension through the EnvironmentExecutionCondition file using the @ExtendWith annotation.
    • Create the EnvironmentExecutionCondition file where all the conditions are going to be specified on implementation of the ExecutionCondition API.

When running tests in the Dev or QA environment, the “add” test will be active and executed, whereas the tests will not run if you are in the Prod environment.

To execute the tests in a given environment, run the appropriate command on VM arguments under the “run configurations” parameter:

  1. Development environment: -ea -Denvironment=Dev
  2. QA environment: -ea -Denvironment=QA
  3. Prod (or Live) environment: -ea -Denvironment=live

Read – How To Run Junit Tests From The Command Line

How to create JUnit 5 extensions by implementing TestInstanceFactory

We can create JUnit 5 extensions by implementing the TestInstanceFactory API for creating test class instances. These should run before the execution of each test method.

The created test instance can then be acquired from a dependency injection framework or by invoking a static factory method to create it.

The following JUnit 5 extension example demonstrates the use of test instance factories on outer and inner classes:

How to test lifecycle callbacks in JUnit 5

Lifecycle callbacks are functions that are automatically executed before or after certain model methods. For example, you can use lifecycle callbacks to automatically compute the value of a ‘full name’ attribute before creating or updating a user record.

Lifecycle methods and Test instance lifecycle

In the primary test instance lifecycle, JUnit 5 defines class and method’s lifecycle driven by the following annotations:

  1. @BeforeAll
  2. @BeforeEach
  3. @AfterEach
  4. @AfterAll

Methods annotated with @BeforeAll and @AfterAll should be executed before and after all test methods in the class. On the other hand, methods annotated by @BeforeEach and @AfterEach should be executed respectively before and after each test method.

JUnit creates a new instance for the test class before running each test in the test instance lifecycle. This behavior aims to run each test separately and thus avoid the side effects of running other tests.

The above execution gives the following result:

From the test execution result, the default behavior is the ‘Per Method Lifecycle’:

Per Method Lifecycle

The default behavior of the test life cycle can be changed using the @org.junit.jupiter.api.TestInstance API, which allows the change of the default lifecycle (for a test class or a test method). This can be done by adding @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation to the test class.

Here is the updated execution result after the modification of default behavior (of the test life cycle):

From the test execution result, the modified behavior gives the ‘Per Class Lifecycle’:

Per Class Lifecycle

JUnit 5 Extension Lifecycle

In addition to the per class and per method lifecycle, JUnit 5 Jupiter offers different interfaces that define APIs for extending tests at various points in the execution lifecycle. JUnit 5, therefore, calls extensions callbacks to implement the behavior.

The APIs are a part of the org.junit.jupiter.api.extension package. Here are the APIs that define the extension lifecycle:

  • AfterAllCallback
  • AfterEachCallback
  • BeforeAllCallback
  • BeforeEachCallback

We can create an extension applied to a test class by implementing the BeforeAllCallback, AfterAllCallback, BeforeEachCallback, and AfterEachCallback interfaces.

Here is how to apply the said extension point to a test class:

Here is the execution result:

Test instance post-processing in JUnit 5

The Juniper extensions model provides the ability to post-process test instances after creating test instances by implementing the TestInstancePostProcessor interface. As per the test instance factory, it can invoke the initialization method on the test instance by using, for example, injection dependencies into the instance to use the test instance post-procession.

To illustrate this, We take the case of a logging system from the log4j API, which executes and writes logs after each test execution. Let’s check further details in this JUnit 5 exception example:

Test Instance Pre-destroy Callback in JUnit 5

The extension model also defines the API for extensions that need to be processed between the test instances and their final destruction. For example, the test instance pre-destroy callback is commonly used in cases like dependencies injection cleanup after their usage in a test instance.

Parameter Resolution in JUnit 5

Most of the test methods don’t have parameters. We use the ParameterResolver interface when using parameters, which defines the API org.junit.jupiter.api.extension.ParameterResolver for extensions. It provides the functionality to resolve parameters at runtime dynamically.

The following constructors and annotated methods of a test class can then have one or more parameters:

  1. @Test
  2. @TestFactory
  3. @BeforeEach
  4. @AfterEach
  5. @BeforeAll
  6. @AfterAll

The parameter resolution can be made through name, type, annotation, or a combination of the same. JUnit 5 implements dependency injection using parameters for constructors and methods of test classes to make this possible.

These parameters must be resolved at runtime by an instance of the ParameterResolver type that needs to be registered previously.

By default, JUnit 5 automatically registers ParameterResolver using the three-built-in resolvers:

  • TestInfoParameterResolver: Used to resolve, inject an instance of type TestInfo, and obtain information about the test whose execution is in progress.
  • RepetitionInfoParameterResolver: Used to inject an instance of type RepetitionInfo only for repeated tests.
  • TestReporterParameterResolver: Used to inject an instance of type TestReporter by allowing it to add useful information to the test report.

In case you are using JUnit 4, you can check out our detailed blog that deep dives into Parameterization in JUnit for Selenium Automation.

Exception Handling in JUnit 5

The TestExecutionExceptionHandler interface defines the API that implements extensions that let you fully customize the behavior of a test case when an exception is thrown.

In continuation to the earlier JUnit 5 extension example, we have used the ArithmeticException on the divide test case to create a test class as shown below:

It is extended to an exception handler class for handling the exception that is thrown by the divide operation (when handling with the division by zero):

It is possible to use the traditional method of throwing an exception (using try…catch, Rules, etc.) or through annotations by implementing the TestExecutionExceptionHandler interface.

Read – Mastering Selenium Testing With JUnit Asserts

Third-party Framework Extensions in JUnit 5

The principle behind JUnit is to provide an easily extensible basic framework that allows users to act faster than API developers. This feature makes it possible to build APIs that serve as a basis for third-party libraries.

Though JUnit 5 has a number of third-party extensions, we will cover the following extensions as they are widely used by the developer community:

  • MockitoExtension
  • Selenium-Jupiter
  • Spring TestContext: SpringExtension for Jupiter

1. MockitoExtension

JUnit 5 is best suited for running unit tests. However, when performing integration testing between modules (or interdependent resources) and interaction verification, stubs or mocks are used to simulate (or represent) the dependent or unavailable resources. Mockito is a framework that allows the creation of mock objects for integration testing.

Here are the major ways in which you can use MockitoExtension:

  1. Manual approach
  2. Using annotations
  3. Using JUnit 5 extensions that are available in the mockito-junit-jupiter artifact (Most preferred option)

The use of the Mockito extension can be seen by applying the extension by adding @ExtendWith to the test class and annotating the simulated fields with @Mock.

For example, if we need to test the class SERVICE and mock the database, we need to use the following code:

The test class then will look like this:

2. Selenium-Jupiter

By combining the strength of Selenium, the most popular web browser testing framework, and the power of JUnit 5, selenium-jupiter allows creating Selenium tests using local and/or remote browsers. With this, you can run different types of tests for verifying the functionality of web and mobile applications. In addition, the selenium-jupiter extension can be used for Selenium automation testing.

Perform Selenium Automation Testing On Cloud With JUnit Framework.

The following dependency should be used for Maven Projects:

Selenium-Jupiter can be used by simply using the @ExtendWith annotation on the SeleniumJupiter interface for performing cross browser compatibility testing. Here is a sample demonstration:

Read – Automated Testing With JUnit And Selenium For Browser Compatibility

How to use Selenium-Jupiter for Selenium Automation Testing

Selenium-Jupiter supports testing remote web browsers on a Selenium Grid through the combination of DriverCapabilities & RemoteWebDriver. You can also perform parallel testing in Selenium by running tests on different browser & platform combinations using LambdaTest.

How to use Selenium-Jupiter for Mobile Device Testing

To create an instance of ApiumDriver to drive mobile devices, the annotation DriverCapabilities. Selenium-Jupiter will automatically start an instance of the Appium server.

How to use Selenium-Jupiter to perform Selenium Automation testing on Cloud Grid

Selenium-Jupiter lets you run Selenium automation tests on a cloud-based cross browser testing platform like LambdaTest. The major benefits of cloud testing are improved browser coverage, elimination of environment-related schedule delays, improved product quality, and reduced Total Cost of Ownership (TCO). Check out our cloud testing tutorial covering the innumerable benefits of migrating tests to a cloud Selenium Grid like LambdaTest.

After you create an account on LamdaTest, note the username & access from the LambdaTest profile section. These credentials are required for accessing the cloud grid. Then, you can generate the desired capabilities using the LambdaTest Capabilities Generator.

Shown below is an example of running JUnit 5 test on the LambdaTest Grid:

Here is the execution snapshot that indicates that the test execution was successful.

automation testing

3. Spring TestContext: SpringExtension for Jupiter

Introduced in Spring 5, Spring TestContext is a Spring framework that offers full integration with the JUnit 5 Jupiter programming model. It can be found in the org.springframework.test.context.junit.jupiter.SpringExtension package.

It can be used by simply annotating the JUnit Jupiter test class with any one of the following annotations:

  1. @ExtendWith(SpringExtension.class)
  2. @SpringJunitConfig(TestConfig.class)
  3. @SpringJUnitWebConfig(TestConfig.class)

Shown below is a JUnit 5 Extension Example that demonstrates the usage of Spring TestContext:

Conclusion and recommendations

The JUnit 5 extension model built into Jupiter has solved inherent problems in JUnit 4 extension points. The model implements multiple built-in extension points and allows their customization and grouped use. This allows extension developers to implement interfaces in one of the existing ones to incorporate extra capabilities for JUnit 5.

JUnit 5 extensions allow enhancing and extending JUnit capabilities. However, some frameworks also have fully integrated and adapted JUnit extension points allowing their reuse, making the Jupiter extension model more powerful, and simplifying tests according to the environments and situations. Therefore, it is strongly recommended to use the extension points, whether integrated or customized, to make the tests more reliable.

Source

This article does not exhaustively present all the extension points integrated with JUnit 5 or even all the extensions of third-party libraries. Therefore, if you are interested in an extension point or a third-party extension framework that is not shown here, you can let us know to complete this guide according to the readers’ interests.

We can also develop a little more in detail those which do not seem clear to you in this guide. We are also interested in your feedback on using JUnit Jupiter extension points in your respective projects. The source code of the above examples can be found on GitHub.

Frequently Asked Questions

What is JUnit 5 extension?

JUnit 5 extensions introduce an entirely new method of extending and configuring unit test behavior. An extension point refers to a method that’s invoked when certain conditions or events are met during the course of running an automated test.

What is the extension of JUnit test files?

JUnit test files are written in files with the .java file extension.

How do I add JUnit 5 to the build path?

Follow the below-mentioned steps to add JUnit 5 library files in the Eclipse IDE:

  1. Right-click the project folder and choose Configure Build Path…
  2. Select the ‘Libraries’ tab under the JRE Library, and add JUnit-related jar files if not added.

Free Testing

Author Profile Author Profile Author Profile

Author’s Profile

Ghislain Kalonji Mukendi

QA and Devops Engineer helping companies set up processes, tools and teams from scratch and help them scale to ISO 9001 quality management.

Blogs: 1



linkedintwitter

Test Your Web Or Mobile Apps On 3000+ Browsers

Signup for free