Introduction to JUnit 5 Extensions

This is a free sample lesson of my Introduction to JUnit 5 course. My JUnit 5 course has 24 lessons, 47 exercises, and 13 quizzes which help you to work smarter and save time when you are writing tests with JUnit 5.

After you have finished this lesson, you:

  • Know what a JUnit 5 extension is.
  • Can identify the invocation order of extensions, a test class constructor, lifecycle callback methods, and test methods.
  • Understand how you can register JUnit 5 extensions.

Let's begin.

What Is a JUnit 5 Extension?

A JUnit 5 extension is a component that can be invoked by JUnit 5 when the execution of a test reaches a specific phase (aka an extension point). When the execution of a test reaches an extension point, JUnit 5 invokes all registered extensions which can be run at that extension point.

If you want to create an extension that can be run at a specific extension point, you have to create an extension class that implements a specific interface provided by the JUnit 5 Extension API. JUnit 5 supports these extension points:

  • Test instance pre-construct callback. If you write an extension which is run at this extension point, JUnit 5 runs your extension before it creates a new test instance.
  • Test instance factory. You should use this extension point if you have to write an extension that creates new test instances.
  • Test instance post-processing. If you write an extension which is run at this extension point, JUnit 5 runs your extension after it has created a new test instance.
  • Test lifecycle callbacks. If you use this extension point, you can write an extension that's invoked at different phases of the test execution lifecycle. You can attach your extension to these phases of the test execution lifecycle:
    • Your extension is run once before the tests of a test container are run.
    • Your extension is run before a test method and the possible setup (@BeforeEach) methods are run.
    • Your extension is run after the possible setup (@BeforeEach) methods have been run and before a test method is run.
    • Your extension is run after a test method have been run and before the possible teardown (@AfterEach) methods are run.
    • Your extension is run after a test method and the possible teardown (@AfterEach) methods have been run.
    • Your extension is run once after all tests of a test container have been run.
  • Test instance pre-destroy callback. You should use this extension point if you have to write an extension that's run after your test instances have been used by JUnit 5 and before they are destroyed.
  • Conditional test execution. You should use this extension point if you have to write an extension which decides if your test methods should be invoked.
  • Parameter resolution. If you write an extension which is run at this extension point, JUnit 5 runs your extension when it has to resolve a parameter of a test class constructor, test method, or lifecycle callback (@BeforeAll, @BeforeEach, @AfterEach, and @AfterAll) method at runtime.
  • Exception handling. You should use this extension point if you have to write an extension which intercepts and handles extensions thrown during the test execution.
  • Test result processing. If you write an extension which uses this extension point, JUnit 5 runs your extension when it reports the result of a test method execution.

The following figure demonstrates the invocation order of extensions, a test class constructor, lifecycle callback methods, and test methods when the test class uses the default test instance lifecycle and has a no-argument constructor, all lifecycle callback methods, and one test method (click the image to see the original image):

The invocation order of JUnit 5 extensions
The next lessons of this topic will describe how you can write extensions which can be run at commonly used extension points. In other words, you shouldn't worry if you feel a bit overwhelmed.

Next, you will learn how you can register JUnit 5 extensions.

Registering JUnit 5 Extensions

You can register JUnit 5 extensions declaratively, programmatically, or automatically. Let's move on and take a closer look at these extension registration mechanisms.

Registering Extensions Declaratively

If you want to register extensions declaratively, you have to annotate a test interface, test class, or test method with the @ExtendWith annotation and configure the registered extension. Also, if you are using JUnit Jupiter 5.8 or newer, you can also annotate fields and parameters of test class constructors, test methods, and lifecycle callback (@BeforeAll, @BeforeEach, @AfterEach, and @AfterAll) methods with the @ExtendWith annotation.

Let's take a look at some examples which demonstrate how you can register JUnit 5 extensions by using this extension registration mechanism.

Example 1: Register an extension for all tests of a test class

If you want a register an extension for all tests of a test class, you have to annotate the test class with the @ExtendWith annotation and configure the registered extension. For example, if you want to register an extension called: MyExtension, the source code of your test class looks as follows:

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MyExtension.class)
class MyTest {

}

Example 2: Register an extension for all tests of a nested test class

If you want to register an extension for all tests of a nested test class, you have to annotate the nested test class with the @ExtendWith annotation and configure the registered extension. For example, if you want to register an extension called: MyExtension for all tests of the NestedTestClassA class, the source code of your test class looks as follows:

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.extension.ExtendWith;

class MyTest {

	@Nested
	@ExtendWith(MyExtension.class)
	class NestedTestClassA {

	}

	@Nested
	class NestedTestClassB {

	}
}

Example 3: Register an extension for one test method

If you want to register an extension for one test method, you have to annotate the test method with the @ExtendWith annotation and configure the registered extension. For example, if you want to register an extension called: MyExtension, the source code of your test class looks as follows:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

class MyTest {

	@Test
	@ExtendWith(MyExtension.class)
	void test() {

	}
}

Example 4: Register an extension for a field

If you want to register an extension for a field, you have to annotate the field with with the @ExtendWith annotation and configure the registered extension. For example, if you want to register an extension called: RandomNumberExtension, the source code of your test class could as follows:

import org.junit.jupiter.api.extension.ExtendWith;

class MyTest {

	@ExtendWith(RandomNumberExtension.class)
	private static int randomNumberOne;

	@ExtendWith(RandomNumberExtension.class)
	private int randomNumberTwo;

}

As you might have noticed, the MyTest class has two fields and one of them is static. The difference between static and non-static fields is described in the following:

  • If a field is static, you can use it everywhere in the test class, including all lifecycle callback methods.
  • If a field isn't static, you can use it everywhere in the test class, except in the @BeforeAll and @AfterAll lifecycle callback methods.

Example 5: Register an extension for a parameter

If you want to register an extension for a parameter of a of test class constructor, test method, or lifecycle callback method, you have to annotate the parameter in question with the @ExtendWith annotation and configure the registered extension. For example, if you want to register an extension called: MyExtension for a parameter of a test method, your test class looks as follows:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

class MyTest {

	@Test
	void test(@ExtendWith(MyExtension.class) String myParameter) {

	}
}

Example 6: Register multiple extensions

If you want to register multiple extensions, you can use one of these two options:

1. You can use one @ExtendWith annotation and pass the registered extensions as the value of the @ExtendWith annotation's value attribute. For example, if you have to register the extensions: MyExtensionOne and MyExtensionTwo, the source code of your test class looks as follows:

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith({MyExtensionOne.class, MyExtensionTwo.class})
class MyTest {

}

2. Because the @ExtendWith annotation is a repetable annotation, You can also use multiple @ExtendWith annotations. For example, if you have to register the extensions: MyExtensionOne and MyExtensionTwo, the source code of your test class looks as follows:

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MyExtensionOne.class)
@ExtendWith(MyExtensionTwo.class)
class MyTest {

}
If you register multiple extensions by using the @ExtendWith annotation, the registered extensions will be run in the order in which they are declared in the source code. For example, the tests found from the MyTest class will be extended by the MyExtensionOne and MyExtensionTwo extensions (in this order).

Example 7: Register an extension by using a custom annotation

You can create a custom annotation that registers JUnit 5 extensions. This technique is useful if you want to register multiple extensions in a reusable way or if you have to create an annotation that registers an extension and identifies the target of the registered extension. For example, let's assume that you want to create a custom annotation which can be applied to a field and which registers the RandomNumberExtension for the target field.

After you have written your custom annotation, its source code looks as follows:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface RandomNumber {

}

You can now register the RandomNumberExtension for the fields of your test class by annotating these fields with the @RandomNumber annotation. After you have done this, the source code of your test class could look as follows:

class MyTest {

	@RandomNumber
	private static int randomNumberOne;

	@RandomNumber
	private int randomNumberTwo;

}

Let's move on and find out how you can register JUnit 5 extensions by using the programmatic extension registration mechanism.

Registering Extensions Programmatically

If you register your extension declaratively by using the @ExtendWith annotation, typically you have to configure the registered extension by using annotations. If you want to configure the registered extension programmatically (by using the constructor of the registered extension, a static factory method, or a builder), you have to follow these steps:

  1. Add a new extension field to your test class.
  2. Create a new extension object and store the created object in the extension field.
  3. Annotate the extension field with the @RegisterExtension annotation.
If you have extensions which are registered for fields with the @ExtendWith annotation and extensions which are registered programmatically, these extensions will be ordered by using an deterministic algorithm, but order of these extensions isn't obvious. Thus, if you want that your extensions are registered in explicit order, you have annotate the @ExtendWith and @RegisterExtension fields with the @Order annotation.

When you register an extension declaratively, you can declare either a static or non-static extension field. The difference between these two options is described in the following:

1. If the extension field is static, JUnit 5 will register the extension after the extensions which are registered at class level by using the @ExtendWith annotation. If you want to register an extension called: MyExtension by using the programmatic extension registration mechanism, the source code of your test class looks as follows:

import org.junit.jupiter.api.extension.RegisterExtension;

class MyTest {

	@RegisterExtension
	private static MyExtension extension = MyExtension.getBuilder()
		.configurationOption("option")
		.build();
}

2. If the extension field is non-static, the extension will be registered after the test class has been instantiated and the created object has been post-processed (all registered TestInstancePostProcessor objects have been run). If you want to register an extension called: MyExtension by using the programmatic extension registration mechanism, the source code of your test class looks as follows:

import org.junit.jupiter.api.extension.RegisterExtension;

class MyTest {

	@RegisterExtension
	private MyExtension extension = MyExtension.getBuilder()
		.configurationOption("option")
		.build();
}
If you have extensions which are registered for methods with the @ExtendWith annotation, the instance extensions will registered after the extensions which are registered declaratively. However, if you are using the test lifecycle mode: Lifecycle.PER_CLASS, the instance extensions will be registered before the extensions which are registered declaratively.

Additional Reading:

Next, you will learn how you can use the automatic extension registration mechanism.

Registering Extensions Automatically

The automatic extension registration mechanism uses the Java's ServiceLoader mechanism and it allows JUnit 5 to auto-detect the extensions found from the classpath and register the found extensions automatically. If you want to leverage the automatic extension registration mechanism, you have to follow these steps:

  1. Ensure that the jar file which contains the registered extensions has the /META-INF/services directory.
  2. Add the org.junit.jupiter.api.extension.Extension file to the /META-INF/services directory.
  3. Add the fully qualified class names of the registered extensions to the org.junit.jupiter.api.extension.Extension file.
  4. Enable the automatic extension registration mechanism by setting the value of the junit.jupiter.extensions.autodetection.enabled configuration option to true. You can either put it to the JUnit platform configuration file or pass it to the JVM by using a system property (-Djunit.jupiter.extensions.autodetection.enabled=true).

You know what a JUnit 5 extension is, understand when different extensions are run by JUnit 5, and can register JUnit 5 extensions. Let's summarize what you learned from this lesson.

This is a free sample lesson of my Introduction to JUnit 5 course. My JUnit 5 course has 24 lessons, 47 exercises, and 13 quizzes which help you to work smarter and save time when you are writing tests with JUnit 5.

Summary

This lesson has taught you six things:

  • A JUnit 5 extension is a component that can be invoked by JUnit 5 when the execution of a test reaches a specific phase (aka an extension point).
  • If you want to register a JUnit 5 extension declaratively, you have to use the @ExtendWith annotation.
  • If you register your extension declaratively, typically you have to configure the registered extension by using annotations. If you register the extension programmatically, you can configure the registered extension by using the constructor of the registered extension, a static factory method, or a builder.
  • If you want to register an extension programmatically, you have add a new extension field to your test class, create a new extension object and store the created object in the extension field, and annotate the extension field with the @RegisterExtension annotation.
  • The automatic extension registration mechanism uses the Java’s ServiceLoader mechanism and it allows JUnit 5 to auto-detect the extensions found from the classpath and register the found extensions automatically.
  • The automatic extension registration mechanism isn't enabled by default.

Get the sample code from Github

0 comments… add one

Leave a Reply