Article

JUnit extensions for Spring tests

Custom JUnit 5 extensions that can improve the performance and reliability of your Spring integration tests.

Florian Beaufumé
Florian Beaufumé LinkedIn X GitHub
Published 20 Apr 2025 - 5 min read
JUnit extensions for Spring tests

Table of contents

Introduction

JUnit 5 extensions are the descendant of JUnit 4 rules. They are used to extend the behavior of JUnit tests. They take the form of dedicated classes that can be declared in your test classes. They support various types of features. For example, they can be used to execute some code before or after test methods or test classes.

In this article, I will describe several custom JUnit extensions that can be useful in Spring tests to improve the performance and reliability.

Note that this article is part of a test oriented series:

  1. Getting started with Spring tests provides best practices and tips to get started with Spring tests.
  2. JUnit extensions for Spring tests (this article) describes several useful JUnit 5 extensions for Spring tests.
  3. Switching between H2 and Testcontainers in Spring tests describes how to easily switch between H2 and Testcontainers in Spring tests.
  4. Security annotations in Spring tests focuses on security annotations used in Spring tests.

A GitHub sample repository link is provided at the end of this article.

Log each test duration

We will first write a basic extension that logs the duration of each test method and test class. It does not require Spring, so can be used in any JUnit 5 test class. Many IDE already display these durations when they execute the tests, so it is mostly a getting started example.

The extension is:

public class DurationExtension implements BeforeAllCallback, AfterAllCallback,
BeforeEachCallback, AfterEachCallback {

private static final Logger logger = LoggerFactory.getLogger(DurationExtension.class);

private long classTimestamp;

private long methodTimestamp;

@Override
public void beforeAll(ExtensionContext context) {
classTimestamp = System.currentTimeMillis();
}

@Override
public void afterAll(ExtensionContext context) {
logger.info("Executed class {} in {} ms",
context.getDisplayName(),
System.currentTimeMillis() - classTimestamp);
}

@Override
public void beforeEach(ExtensionContext context) {
methodTimestamp = System.currentTimeMillis();
}

@Override
public void afterEach(ExtensionContext context) {
logger.info("Executed method {}.{} in {} ms",
context.getParent().map(ExtensionContext::getDisplayName).orElse(""),
context.getDisplayName(),
System.currentTimeMillis() - methodTimestamp);
}
}

Notice that the extension implements several interfaces. This allows to execute some code before and after each test method or test class. In the "before" methods, I store the current timestamp, and in the "after" methods I display the duration.

Then to use it in a given test class:

@ExtendWith(DurationExtension.class)
public class MyTest {

@Test
void someTest() {
// ...
}

@Test
void someOtherTest() {
// ...
}
}

Here is a sample log when the tests are executed:

INFO c.a.sample.extension.DurationExtension   : Executed method MyTest.someTest() in 2 ms
INFO c.a.sample.extension.DurationExtension : Executed method MyTest.someOtherTest() in 1 ms
INFO c.a.sample.extension.DurationExtension : Executed class MyTest in 5 ms

Log total tests duration

Now what if we want to log the total duration of all test classes? We need to execute some code once before all tests and once after all tests. The JUnit solution uses the TestExecutionListener interface:

public class DurationListener implements TestExecutionListener {

private static final Logger LOGGER = LoggerFactory.getLogger(DurationListener.class);

private static long startTimestamp = 0;

@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
startTimestamp = System.currentTimeMillis();
}

@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
long duration = System.currentTimeMillis() - startTimestamp;
LOGGER.info("Tests executed in {} s", String.format("%d.%03d", duration / 1000, duration % 1000));
}
}

This listener also does not require Spring. It implements two methods, one that is executed before all tests and one that is executed after all tests. This DurationListener is not declared on test classes with @ExtendWith. Instead, it is globally declared in a src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener file containing the FQCN (fully qualified class name) of the listener:

com.adeliosys.sample.test.DurationListener

In addition, the org.junit.platform:junit-platform-launcher Maven dependency is needed. For example add to your pom.xml:

<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>

A sample output when the tests are executed:

(...tests logs...)
INFO c.a.sample.test.DurationListener : Tests executed in 12.639 s

Leave a clean database

Test methods and test classes should be independent. They should, for example, not leave data in the database, in order to prevent side effects in other tests.

To isolate test methods, I generally use @Transactional annotations to automatically rollback the changes, as explained in Getting started with Spring tests - A Sample test.

To isolate test classes, I use @AfterAll methods that explicitly execute selected repository.deleteAll() methods to undo the inserts performed in the @BeforeAll methods. This also makes the test code cover the delete methods of my repositories.

You may alternatively go with a JUnit extension that clears the whole database when needed. That are some for most databases.

I you prefer the explicit approach, there is still a gotcha: what if you forgot to call some delete methods? To detect this, we can write an extension based on Spring that checks if the tables are empty, using repository.count() methods.

Here is how to do it:

public class DatabaseCleanupCheckExtension implements AfterAllCallback {

private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseCleanupCheckExtension.class);

@Override
public void afterAll(ExtensionContext context) {
ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);

boolean success = true;

// Lookup all application repositories from Spring
Map<String, JpaRepository> repositories = applicationContext.getBeansOfType(JpaRepository.class);

for (Map.Entry<String, JpaRepository> entry : repositories.entrySet()) {
// We could implement here a mechanism that skips some selected repositories

// Check that the table is empty
long count = entry.getValue().count();
if (count > 0) {
LOGGER.error("Repository '{}' found {} row{} instead of 0",
entry.getKey(), count, count > 1 ? "s" : "");
success = false;
}
}

// Log the result
if (success) {
LOGGER.info("Database cleanup is ok");
}
else {
fail("Database cleanup verifications failed");
}
}
}

The extension only implements the AfterAllCallback callback, to perform the verification after all test methods of a class are executed. The Spring application context is extracted from the JUnit context using SpringExtension.getApplicationContext(context) utility method. Then we can lookup all repository beans with applicationContext.getBeansOfType(JpaRepository.class), and execute their count() method. If at least one repository returns a strictly positive count, we log some error message and use fail() to make sure that the test class fails.

A sample log when the verification succeeds:

INFO c.a.s.e.DatabaseCleanupCheckExtension    : Database cleanup is ok

And when the verification fails:

ERROR c.a.s.e.DatabaseCleanupCheckExtension   : Repository 'fooRepository' found 3 rows instead of 0
ERROR c.a.s.e.DatabaseCleanupCheckExtension : Repository 'barRepository' found 1 row instead of 0

The extension code sample uses all available repositories, but you could ignore some of them if needed, for example if the database was initialized with some data that should not be deleted.

Track the Spring application contexts

As explained in Getting started with Spring tests - Reusing Spring application contexts, Spring Test tries his best to cache and reuse the application contexts over the test classes, in order to improve the performances. But depending on the test classes configuration, new application contexts may be built, increasing the tests duration and memory usage.

In this section I will describe a JUnit extension that tracks and logs the application contexts usage by the test classes. It can help figure out how to reduce the number of application contexts.

The simplified extension is (see repository link at the end of the article for the full version):

public class SpringContextTrackerExtension implements BeforeAllCallback, TestExecutionListener {

private static final Logger LOGGER = LoggerFactory.getLogger(SpringContextTrackerExtension.class);

// For each Spring application context, the test classes using it.
// Application contexts are identified by their startup timestamp, it is usually good enough.
private static final Map<Long, List<String>> springContextDescriptors = new HashMap<>();

@Override
public void beforeAll(ExtensionContext context) {
ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);

// Record the Spring application context usage
springContextDescriptors.computeIfAbsent(
applicationContext.getStartupDate(), k -> new ArrayList<>())
.add(context.getDisplayName());
}

@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
LOGGER.info("Spring contexts usage:");

// For each application context, log the test classes using it
AtomicInteger index = new AtomicInteger(0);
springContextDescriptors.forEach((timestamp, testClasses) -> {
String testClassesSummary = String.join(", ", testClasses);

LOGGER.info("- Context #{} is used by {} test class{}: {}",
index.incrementAndGet(), testClasses.size(),
testClasses.size() > 1 ? "es" : "", testClassesSummary);
});
}
}

A static Map is used to store the application contexts usage, i.e. for each application context (actually identified by its startup time) the name of the test classes. The map is updated in the beforeAll callback, which is executed once before each test class. In the testPlanExecutionFinished callback, executed once all test classes were executed, we log the application contexts usage.

A sample output is:

INFO c.a.s.t.SpringContextTrackerExtension    : Spring contexts usage:
INFO c.a.s.t.SpringContextTrackerExtension : - Context #1 is used by 3 test classes: ItemControllerClientTest, ItemControllerServerTest, ItemServiceTest
INFO c.a.s.t.SpringContextTrackerExtension : - Context #2 is used by 1 test class: ItemControllerSliceTest

We see that two application contexts are used by four test classes. The first application context is used by three classes, while the second one is used by a single class. So, maybe we could have a look at that class to see if it really needs a dedicated application context.

Conclusion

JUnit 5 extensions are a powerful way to extend the behavior of JUnit tests. In this article I explained the basis of extensions development and described several extensions that can be useful when using Spring tests.

For a complete example, see sample GitHub project spring-tests.

© 2007-2025 Florian Beaufumé