What the Heck Is the SpringExtension Used For?

Last Updated:  August 27, 2021 | Published: July 27, 2021

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 UUIDs 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:

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:

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:

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:

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:

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.

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:

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:

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:

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 SpringExtensionto work with a Spring TestContext throughout the test:

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:

The source code for the examples above is available on GitHub.

Have fun using the SpringExtension,

Philip

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
>