I’ve seen a lot of confusion recently about the SpringExtension. When failing to get the context configuration for a test right, some developers randomly throw @ExtendWith(SpringExtension.class)
to their test classes to (hopefully) get their tests running. With this blog post, I want to shed some light on the SpringExtension and its usage when testing Spring Boot application. At the end of this article, you’ll understand when, why, and how to use this extension.
TL;DR: The SpringExtension
enables seamless integration of JUnit Jupiter tests with Spring’s TestContext framework. Most of the time, you don’t need to explicitly register the SpringExtesion, as all Spring Boot test slice annotations already do this.
What’s a JUnit Jupiter Extension Used For?
The first question we have to answer when we want to understand the SpringExtension
is what a JUnit Jupiter extension is all about.
The JUnit Jupiter extension model is a single concept (in contrast to JUnit 4’s Runner
and Rule
API) to enhance testing functionality and intercept the lifecycles of our tests programmatically. There are many different extension points for both the lifecycle (e.g., BeforeAllCallback
) and other utility functions (e.g., ParameterResolver
) available. For a complete list of all available extension APIs, please take a look at the Extension
interface and all interfaces that extend it.
Their usage is versatile. We can do housekeeping tasks before or after a test, resolve parameters, or decide whether to execute a test or skip it.
Most of the time, we implement cross-cutting concerns that relate to multiple tests with an extension. For example, when writing web tests, instead of wrapping the browser interaction with a try-catch block for every test to make screenshots on failure, we could write an extension for this. JUnit Jupiter offers the TestExecutionExceptionHandler
interface to handle exceptions for our test methods at a central place. (PS: Selenide already comes with such a screenshot extension on failure).
Writing a custom extension is no rocket science. As part of the Testing Spring Boot Applications Masterclass, we develop a custom extension to inject random UUID
s to our test methods.
Whenever we want to activate a JUnit Jupiter extension for our test, we have to explicitly register the extension with @ExtendWith
on top of the test class:
1 2 3 4 5 6 7 |
@ExtendWith(MyExtension.class) class MyTest { @Test void test() { } } |
Many frameworks and testing libraries ship with a custom JUnit Jupiter extension for convenient integration with the JUnit environment.
Examples are:
- Mockito’s
MockitoExtension
for a seamless setup of our mocks - Testcontainers
TestcontainersExtension
(activated with@Testcontainers
) to conveniently start and stop Docker containers - Spring’s
SpringExtension
For more information about the extension model and its various APIs, consult the JUnit 5 User Guide.
What’s the Purpose of the SpringExtension?
As a next step, let’s investigate the cross-cutting functionality the SpringExtension
implements.
The best way to start our investigation is to take a look at the source code of the SpringExtension
to understand which extension APIs it implements:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { // ... } |
That’s a lot. As we can see from the source code above, the SpringExtension
is heavily involved in the lifecycle of our tests.
Without diving too deep into the implementation, the main responsibilities of this extension are the following:
- manage the lifecycle of the Spring
TestContext
(e.g., start a new one or get a cached context) - support dependency injection for parameters (e.g., test class constructor or test method)
- cleanup and housekeeping tasks after the test
The SpringExtension
acts as a glue between JUnit Jupiter and Spring Test. Most of the time the SpringExtension
delegates its responsibilities to the TestContextManager
to do the heavy lifting:
1 2 3 4 5 6 7 |
public class SpringExtension { @Override public void beforeAll(ExtensionContext context) throws Exception { getTestContextManager(context).beforeTestClass(); } } |
The TestContextManager
is responsible for a single TestContext
and test framework agnostic. Furthermore, the TestContextManager
also invokes all registered TestContextListeners
based on the lifecycle event (e.g., before test class or after a test method).
Let’s take a look at another practical example of the SpringExtension
in-action: Resolving (aka. injecting) parameters:
1 2 3 4 5 6 7 8 9 10 |
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @Test void needsEnvironmentBeanToVerifySomething( @Autowired Environment environment) { // resolved by the SpringExtension assertNotNull(environment); } } |
The test above injects the Environment
via a method parameter. In the background, the following code snippet of the SpringExtension
is responsible for resolving this parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class SpringExtension { // ... @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { // determine whether or not this extension is responsible to resolve the parameter } @Nullable public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { // return the bean from the TestContext } } |
Before the SpringExtension
tries to resolve a given parameter, the supportsParameter
(part of the ParameterResolver
interface) determines if it’s the responsibility of this extension. In our example, the @Autowired
annotation next to the parameter is a clear indicator for the SpringExtension
to resolve this parameter from the TestContext
.
Field injection, however, works via the DependencyInjectionTestExecutionListener
. This is one of the many default TestExecutionListeners
that the TestContextManager
invokes before running any test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @Autowired // injected by the DependencyInjectionTestExecutionListener private CustomerService customerService; @Test void needsEnvironmentBeanToVerifySomething( @Autowired Environment environment // resolved by the SpringExtension ) { assertNotNull(environment); } } |
When Do We Need To Register the SpringExtension?
Most of the time, we don’t need to explicitly register this extension because it’s already activated for us. This is the case whenever we use a Spring Boot test annotation.
All Spring Boot test slice annotations and also @SpringBootTest register the SpringExtension
out-of-the-box. This is beneficial, as otherwise, the tests would fail to start as they require a TestContext to work with. Hence this opinionated approach saves us some keystrokes, and we don’t encounter weird test failures because we’ve forgotten to register the SpringExtension
.
We can see this by taking a look at the source code of the @WebMvcTest
annotation:
1 2 3 4 5 6 7 8 9 10 |
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(WebMvcTestContextBootstrapper.class) @ExtendWith({SpringExtension.class}) // ... further annotations public @interface WebMvcTest { } |
Among other meta-annotations and instructions on what to auto-configure for this kind of test, we see the SpringExtension
is activated for us.
The same is true for @SpringBootTest
:
1 2 3 4 5 6 7 8 9 10 |
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(SpringBootTestContextBootstrapper.class) @ExtendWith(SpringExtension.class) // ... public @interface SpringBootTest { // ... } |
So whenever we encounter a test class where the SpringExtension
is registered manually, and a Spring Boot test slice annotation is in use, we can safely remove it:
1 2 3 4 5 6 7 |
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ExtendWith(SpringExtension.class) // not necassary and should be removed class ApplicationIT { // ... } |
While JUnit Jupiter won’t complain (aka. fail the test or produce noise in the logs), if the same extension is configured twice, we should remove it to avoid this duplication. It’s redundant code and might only lead to confusion for new team members if we don’t consistently add the SpringExtension
everywhere.
When Do We Need to Register the SpringExtension (Part II)?
There are limited use cases where we have to explicitly register the SpringExtension
manually. One of such use cases is writing a custom test slice annotation.
As part of the Testing Spring Boot Applications Masterclass, we bootstrap a messaging component of our application for testing purposes. This hands-on example demonstrates how to only bootstrap Amazon SQS Listener relevant parts of our application. As there is now Spring Boot test slice annotation available, we manually register the SpringExtension
to work with a Spring TestContext throughout the test:
1 2 3 4 5 6 7 8 9 10 |
@ExtendWith(SpringExtension.class) @Import(BookSynchronizationListener.class) @ImportAutoConfiguration(MessagingAutoConfiguration.class) @Testcontainers(disabledWithoutDocker = true) class BookSynchronizationListenerSliceTest { // test a SQS listener in isolation } |
Furthermore, for projects that use plain Spring without Spring Boot, we’re also in the driver seat and have to take care to register the SpringExtension
.
Summary
The SpringExtension
implements several JUnit Jupiter extension model callback methods for seamless integration between JUnit and Spring. When testing Spring Boot applications, most of the time, we don’t have to explicitly register this extension as all sliced context annotations (e.g. @WebMvcTest)
do this for us.
The official documentation is also an excellent source of information if you want to dive deeper into this topic.
Consider the following blog posts to learn more about Spring Boot’s excellent testing capabilities:
- Spring Boot Unit and Integration Testing Overview
- Spring Boot Test Slices: Overview and Usage
- Guide to @SpringBootTest for Spring Boot Integration Tests
The source code for the examples above is available on GitHub.
Have fun using the SpringExtension
,
Philip