Introduction
JUnit 5 extensions are one of those features many teams rely on every day without ever thinking about them. If you use Mockito, Spring Boot tests, or Testcontainers, extensions are already doing a lot of work behind the scenes for you.
What is far less common is to use the same mechanism deliberately to solve problems in your own test suite. Especially in integration tests, extensions can simplify code, remove repetition, and make tests easier to reason about.
What a JUnit 5 extension really is
A JUnit 5 extension is not just a helper class or a utility method. It is a way to hook into the test execution lifecycle.
When JUnit runs a test, it goes through a well-defined lifecycle. Test classes are discovered, instances are created, methods are executed, and results are collected. At each of these steps, JUnit checks whether there are extensions registered for that test and gives them a chance to participate in the process.
From a technical point of view, an extension is a plain Java class that implements one or more interfaces from the org.junit.jupiter.api.extension package. Each interface represents a specific moment in the test lifecycle. Some extensions run before a test class is instantiated, others after each test method, and others only when an exception is thrown.
Once registered using the @ExtendWith annotation, the extension becomes part of the execution flow. It reacts to events triggered by the engine instead of being explicitly invoked by the test code.
Why does this matter for any type of tests?
Sometimes, tests often fail for reasons that have nothing to do with business logic. Timing issues, asynchronous processing, external services, and unstable environments are common sources of problems.
If this kind of logic is handled directly inside test methods, like the common approach of creating helper classes, tests quickly become noisy and hard to maintain. Extensions allow these concerns to live in one place, outside the test logic, while still being applied consistently across the entire test suite.
Example: Environment availability check
This example is more of a feature of the testing framework than many tools you already use, such as Testcontainers, Mockito, and others, that provide their own JUnit extension.
The problem with environment failures
Let’s think that you have a project that also runs system or end-to-end tests in different environments, like a staging, QA, pre-pro, ephemeral, or whatever environment you have to test. We have all been facing this problem: the environment unavailability.
We run the test, everything fails, the error message is noisy and misleading, and, only after a few seconds or minutes, we realize that the target system is not running. This is not a test failure; this is an infrastructure failure. This failure can be anything: a backend service, a database, or the message broker, and any other thing in the architecture that can prevent the system, or part of it, from being available.
There are some symptoms, like connection refused errors, timeouts, stack traces unrelated to the test intent, and dozens of failed tests hiding a single root cause. The approach we will take in the example is: if the environment is not ready, do not run any test.
Why does this not belong in the test code?
The availability of a backend is not part of the test logic, as tests should describe behavior, not infrastructure health checks.
This makes it a perfect candidate for a JUnit 5 Extension, as it:
- runs before tests
- applies consistently
- keeps test code clean
- fails fast with a clear message
The idea is to make an environment availability a precondition. As we can influence the test lifecycle in JUnit 5 using a custom extension can do the following if the requirements about the environment availability are not met:
- abort the execution
- or skip the tests with a clear explanation
That is exactly what Assumptions.assumeTrue(...) is designed for.
Implementing the extension
The extension below checks whether a backend health endpoint is reachable before any test runs. If the backend is down, tests are skipped immediately with a clear message.
public class EnvironmentAvailabilityExtension implements BeforeAllCallback {
private static final int TIMEOUT = 1000;
private static Boolean isEnvironmentReady;
private static final Logger log = LogManager.getLogger(EnvironmentAvailabilityExtension.class);
@Override
public void beforeAll(ExtensionContext context) {
if (isEnvironmentReady == null) checkEnvironmentHealth();
Assumptions.assumeTrue(isEnvironmentReady,
"Environment is not available. Start it before running the tests."
);
}
private synchronized void checkEnvironmentHealth() {
if (isEnvironmentReady != null) return;
HttpURLConnection connection = null;
try {
URL url = URI.create("http://my-environment-address/api/health").toURL();
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(TIMEOUT);
connection.setRequestMethod("GET");
isEnvironmentReady = (connection.getResponseCode() == HttpURLConnection.HTTP_OK);
} catch (Exception e) {
log.error("Environment is not available. Start it before running the tests.");
isEnvironmentReady = false;
} finally {
if (connection != null) connection.disconnect();
}
}
}
The method checkEnvironmentHealth() will check if the health check of the environment is healthy, which means returning an HTTP status of OK (200); then, it will return true or false. This control is done via the BeforeAllCallback interface, where we add the check that the environment is available, validating it via an Assumption action that will make the test not run. As it does not provide any output, we have logged in the catch that the environment is not available, so we can see it in any log.
How to use it?
Add the @ExtendWith on top of the test class within the EnvironmentAvailabilityExtension class as a parameter.
This will “tell” JUnit to run this extension as part of the test lifecycle, like the @BeforeAll or @BeforeEach.
@ExtendWith(EnvironmentAvailabilityExtension.class)
class OrderTest {
@Test
void shouldCreateOrder() {
// test logic
}
}
Live example
The project restassured-complete-basic-example has the example of the environment availability developed as one of the available test strategies: a custom JUnit Extension that won’t run the tests in the case the health check endpoint is not reachable.
Take a look at the implementation and, maybe, try it out in your local machine: https://github.com/eliasnogueira/restassured-complete-basic-example/blob/main/src/main/java/com/eliasnogueira/credit/extensions/EnvironmentAvailabilityExtension.java
Extensions you are already relying on
Most Java developers already use JUnit 5 extensions, even if they never wrote one themselves.
Mockito uses an extension to initialize mocks, spies, and argument captors.
Spring relies on an extension to bootstrap the application context and manage dependency injection in tests.
Testcontainers integrates with JUnit to control container startup and shutdown predictably.
All of these tools hook into the same extension points shown in the examples above.
Final thoughts
JUnit 5 extensions are not an advanced feature reserved for framework authors. They are a practical way to keep integration tests focused, readable, and consistent.
When you see the same setup code, retry logic, or infrastructure concerns appearing across multiple tests, an extension is often the cleanest solution. Once you start using them intentionally, it becomes hard to imagine maintaining a larger integration test suite without them.
Top comments (0)