 Java, Spring and Web Development tutorials  1. Overview
In this tutorial, we’ll explore how to execute setup code once before all tests across multiple classes in JUnit 5. This is particularly useful for global initializations, such as setting up shared database connections, loading expensive resources, or configuring environments that all tests rely on.
JUnit 5 provides lifecycle annotations like @BeforeAll for per-class setup, but it lacks a built-in mechanism for true “before-global” execution. We’ll discuss the limitations of standard annotations and look at practical solutions using extensions and listeners. These approaches ensure one-time setup and teardown, even in parallel test environments.
2. Understanding JUnit 5 Lifecycle Annotations
Let’s start with a small introduction to JUnit lifecycles. JUnit 5 offers a rich set of annotations to manage test lifecycles, allowing us to control setup and teardown at various levels.
2.1. The Role of @BeforeAll
The @BeforeAll annotation marks a static method to execute once before all @Test methods in a single class. It’s ideal for class-specific setup, such as initializing shared state.
Here’s a basic example:
public class ExampleTest {
@BeforeAll
static void setup() {
System.out.println("Execute: BeforeAll");
// Initialize class-specific resources, e.g., a mock database
}
@Test
void test1() {
System.out.println("Execute test 1");
// Test logic
}
@Test
void test2() {
System.out.println("Execute test 2");
// Test logic
}
}
This runs setup() once per class. The output of this test file would be:
Execute: BeforeAll
Execute test 2
Execute test 1
But for global, one-time execution, @BeforeAll alone isn’t sufficient.
2.2. Limitations for Global Execution
While @BeforeAll works well within a class, JUnit 5 doesn’t provide a direct equivalent for suite-wide setup. Key limitations include:
- Per-Class Scope: It ties to individual classes, leading to redundant executions in multi-class suites.
- No Built-in Suite Support: JUnit 5 emphasizes modularity over traditional suites, so global hooks require custom implementations.
- Parallelism Challenges: In parallel test runs (enabled via junit.jupiter.execution.parallel.enabled=true), concurrent access to shared resources can cause issues without proper synchronization.
These gaps necessitate advanced features like extensions or listeners for true global behavior.
3. Implementing Global Setup With JUnit Extensions
JUnit 5’s extension model is a powerful way to inject custom behavior into the test lifecycle. We can create an extension that simulates a suite-wide setup by ensuring code runs only once.
3.1. Creating a Custom Extension
Let’s implement BeforeAllCallback to hook into the lifecycle before each class and use a flag for one-time execution.
Here is a class version for database setup:
public class DatabaseSetupExtension implements BeforeAllCallback {
private static boolean initialized = false;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
if (!initialized) {
initialized = true;
// Global setup: Initialize database connections
System.out.println("Initializing global database connections...");
// Example: DatabaseConnectionPool.initialize();
}
}
}
JUnit 5 invokes the beforeAll() method of a BeforeAllCallback extension once per test class where the extension is applied. If our test suite has multiple test classes, beforeAll() could be called multiple times. That’s why we need the static variable initialized, which acts as a flag shared across all instances of the extension. It ensures the setup code runs only the first time beforeAll() is called.
To handle parallel tests, let’s add synchronization:
// Add to the class
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void beforeAll(ExtensionContext context) throws Exception {
lock.lock();
try {
if (!initialized) {
// Setup code...
}
} finally {
lock.unlock();
}
}
Using locks ensures our code is thread-safe.
Now we have two versions of an extension, but they won’t do anything until we enable them and make JUnit see them. Let’s see how to do it.
3.2. Registering the Extension Suite-Wide
To apply the extension globally without annotating every class, we have to complete two steps.
First, we enable auto-detection in src/test/resources/junit-platform.properties:
junit.jupiter.extensions.autodetection.enabled = true
Then, we register the extension by creating a src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension file with the content:
com.baeldung.before.all.global.DatabaseSetupExtension
And that’s it! When we execute our ExampleTest now, we get the following output:
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
Alternatively, we could annotate individual classes with @ExtendWith(DatabaseSetupExtension.class). The flag prevents multiple executions.
This approach is flexible and integrates seamlessly with JUnit 5’s lifecycle.
4. Alternative Approaches
While extensions are the most common solution, other mechanisms can achieve similar results depending on our setup.
4.1. Using TestExecutionListener
For a lower-level hook into the entire test plan, we implement TestExecutionListener. This runs testPlanExecutionStarted() before any tests and testPlanExecutionFinished() after all.
Here is how we’d write such a class:
public class GlobalDatabaseListener implements TestExecutionListener {
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
// Global setup
System.out.println("GlobalDatabaseListener # testPlanExecutionStarted ");
// Example: DatabaseConnectionPool.initialize();
}
@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
// Global teardown
System.out.println("GlobalDatabaseListener # testPlanExecutionFinished");
// Example: DatabaseConnectionPool.shutdown();
}
}
Similar to the extension, we have to register it by creating a file src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener with the content:
com.baeldung.before.all.global.GlobalDatabaseListener
When we’re done, here’s the output of our test:
GlobalDatabaseListener # testPlanExecutionStarted
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
GlobalDatabaseListener # testPlanExecutionFinished
We can see that our GlobalDatabaseListener is executed with the method testPlanExecutionStarted() first. When all other test functions finish, the testPlanExecutionFinished() method was called – exactly how we wanted it.
4.2. Using LauncherSessionListener
For even higher-level control over the launcher session (encompassing multiple test plans if applicable), we implement LauncherSessionListener. This is useful in environments with multiple launches, like IDEs:
public class GlobalDatabaseSessionListener implements LauncherSessionListener {
@Override
public void launcherSessionOpened(LauncherSession session) {
// Global setup before session starts
System.out.println("launcherSessionOpened");
}
@Override
public void launcherSessionClosed(LauncherSession session) {
// Global teardown after session ends
System.out.println("launcherSessionClosed");
}
}
To make it work, we have to register it as well, by creating a file src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener with the following content:
com.baeldung.before.all.global.GlobalDatabaseSessionListener
When we run our tests again, we get the following output:
launcherSessionOpened
GlobalDatabaseListener # testPlanExecutionStarted
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
GlobalDatabaseListener # testPlanExecutionFinished
launcherSessionClosed
We can see all our executed tests, including the wrapping statements of our three different “before-globally” implementations.
5. Potential Pitfalls and Best Practices
When implementing global setup – any of our implementations – should consider these pitfalls and tips:
- Thread Safety: Always synchronize shared resources in parallel environments to avoid race conditions.
- Resource Leaks: Ensure teardown is registered properly; test in failure scenarios.
- Test Isolation: Global setup can violate isolation – use transactions or resets in @BeforeEach/@AfterEach for per-test cleanup.
- Build Tool Compatibility: Verify behavior in Maven/Gradle; use plugins like Surefire for parallelism control.
- Debugging: Log extensively and run tests sequentially first (junit.jupiter.execution.parallel.enabled=false) to isolate issues.
- Performance: Minimize global setup time to avoid slowing down the suite.
Following these ensures reliable, maintainable tests.
6. Conclusion
In this article, we learned that JUnit 5’s modular design requires creative solutions for global setup, but extensions and listeners provide robust options. The custom extension approach offers fine-grained control, while listeners handle suite-wide needs at a higher level.
We should choose based on our project’s complexity: extensions for most cases, listeners for broader scopes.
As always, the code is available over on GitHub. The post Run Code Before All Tests in All Classes in JUnit 5 first appeared on Baeldung.
Content mobilized by FeedBlitz RSS Services, the premium FeedBurner alternative. |