JUnit Jupiter provides several extension points that hook into the test execution lifecycle. These hooks allow you to execute logic before and after various phases, from the entire test class container to individual test methods.
Instead of simple print statements, extensions are typically used for cross-cutting concerns like measuring performance, managing external resources, or logging diagnostic data.
Lifecycle Extension points and Use Cases
- BeforeAllCallback: Global setup like starting a Docker container or initializing a shared database connection.
- BeforeEachCallback: Resetting the database state or clearing a cache before every test.
- BeforeTestExecutionCallback: Capturing a high-precision timestamp immediately before the method runs (useful for profiling).
- AfterTestExecutionCallback: Calculating elapsed time for the test logic only.
- AfterEachCallback: General cleanup or logging test results.
- AfterAllCallback: Releasing shared resources or closing the database connection.
Definition of BeforeAllCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface BeforeAllCallback extends Extension {
void beforeAll(ExtensionContext context) 1
throws Exception;
}
Definition of BeforeEachCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface BeforeEachCallback extends Extension {
void beforeEach(ExtensionContext context) 1
throws Exception;
}
Definition of BeforeTestExecutionCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface BeforeTestExecutionCallback extends Extension {
void beforeTestExecution(ExtensionContext context) 1
throws Exception;
}
Definition of AfterTestExecutionCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface AfterTestExecutionCallback extends Extension {
void afterTestExecution(ExtensionContext context) 1
throws Exception;
}
Definition of AfterEachCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface AfterEachCallback extends Extension {
void afterEach(ExtensionContext context) 1
throws Exception;
}
Definition of AfterAllCallbackVersion: 6.0.1 package org.junit.jupiter.api.extension;
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface AfterAllCallback extends Extension {
void afterAll(ExtensionContext context) 1
throws Exception;
}
Execution Order
It is important to understand where extension callbacks fits in the JUnit 5 lifecycle:
BeforeAllCallback (Extension)
@BeforeAll (Test class method)
BeforeEachCallback (Extension)
@BeforeEach (Test method)
BeforeTestExecutionCallback (Extension)
@Test / @ParameterizedTest / @RepeatedTest (Actual test execution)
AfterTestExecutionCallback (Extension)
@AfterEach (Test method)
AfterEachCallback (Extension)
@AfterAll (Test class method)
AfterAllCallback (Extension)
Examples
A Simple Lifecycle Extension
Following example shows a simple extension (nested class) that just prints lifecycle events.
package com.logicbig.example;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;
@ExtendWith(SimpleLifecycleCallbacksTest.SimpleLifecycleExtension.class)
public class SimpleLifecycleCallbacksTest {
@BeforeAll
static void beforeAll(){
System.out.println("Test Class: @BeforeAll method");
}
@BeforeEach
void setup() {
System.out.println("Test Class: @BeforeEach method");
}
@Test
void myTest() {
System.out.println("Test Class: @Test method");
}
@AfterEach
void tearDown() {
System.out.println("Test Class: @AfterEach method");
}
@AfterAll
static void afterAll() {
System.out.println("Test Class: @AfterAll method");
}
static class SimpleLifecycleExtension implements
BeforeAllCallback, AfterAllCallback,
BeforeEachCallback, AfterEachCallback,
BeforeTestExecutionCallback, AfterTestExecutionCallback {
@Override
public void beforeAll(ExtensionContext c) {
System.out.println("Extension Class: BeforeAllCallback"); }
@Override
public void afterAll(ExtensionContext c) {
System.out.println("Extension Class: AfterAllCallback"); }
@Override
public void beforeEach(ExtensionContext c) {
System.out.println("Extension Class: BeforeEachCallback"); }
@Override
public void afterEach(ExtensionContext c) {
System.out.println("Extension Class: AfterEachCallback"); }
@Override
public void beforeTestExecution(ExtensionContext c) {
System.out.println("Extension Class: BeforeTestExecutionCallback"); }
@Override
public void afterTestExecution(ExtensionContext c) {
System.out.println("Extension Class: AfterTestExecutionCallback"); }
}
}
OutputD:\example-projects\junit-5\junit-5-extensions\junit-5-lifecycle-callbacks>mvn test -Dtest=SimpleLifecycleCallbacksTest [INFO] Scanning for projects... [INFO] [INFO] ----------< com.logicbig.example:junit-5-lifecycle-callbacks >---------- [INFO] Building junit-5-lifecycle-callbacks 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-lifecycle-callbacks --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-lifecycle-callbacks\src\main\resources [INFO] [INFO] --- compiler:3.13.0:compile (default-compile) @ junit-5-lifecycle-callbacks --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-lifecycle-callbacks --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-lifecycle-callbacks\src\test\resources [INFO] [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ junit-5-lifecycle-callbacks --- [INFO] Recompiling the module because of changed source code. [WARNING] File encoding has not been set, using platform encoding windows-1252, i.e. build is platform dependent! [INFO] Compiling 4 source files with javac [debug target 17] to target\test-classes [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-lifecycle-callbacks --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- Extension Class: BeforeAllCallback Test Class: @BeforeAll method Extension Class: BeforeEachCallback Test Class: @BeforeEach method Extension Class: BeforeTestExecutionCallback Test Class: @Test method Extension Class: AfterTestExecutionCallback Test Class: @AfterEach method Extension Class: AfterEachCallback Test Class: @AfterAll method Extension Class: AfterAllCallback [INFO] +--com.logicbig.example.SimpleLifecycleCallbacksTest - 0.102 ss [INFO] | '-- [OK] myTest - 0.031 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.038 s [INFO] Finished at: 2025-12-31T14:24:16+08:00 [INFO] ------------------------------------------------------------------------
Let's see a more practical example.
Performance Monitor Extension
In this example, we use the ExtensionContext.Store to pass state (start time) between callbacks without using class variables, ensuring thread safety.
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
public class PerformanceExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final String START_TIME = "START_TIME";
@Override
public void beforeTestExecution(ExtensionContext context) {
context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()))
.put(START_TIME, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) {
long startTime = context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()))
.remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Performance: " + context.getDisplayName() + " took " + duration + "ms");
}
}
Extension simulating database access
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
public class DatabaseExtension implements BeforeAllCallback,
AfterAllCallback, BeforeEachCallback {
@Override
public void beforeAll(ExtensionContext context) {
System.out.println("DB: Starting connection pool");
}
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("DB: Clearing tables for test: " + context.getDisplayName());
}
@Override
public void afterAll(ExtensionContext context) {
System.out.println("DB: Closing connection pool");
}
}
Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith({DatabaseExtension.class, PerformanceExtension.class})
public class PracticalLifecycleTest {
@Test
void loadUserData() throws InterruptedException {
Thread.sleep(100);
System.out.println(" Test: User data loaded");
}
@Test
void processOrder() throws InterruptedException {
Thread.sleep(50);
System.out.println(" Test: Order processed");
}
}
Output$ mvn test -Dtest=PracticalLifecycleTest [INFO] Scanning for projects... [INFO] [INFO] ----------< com.logicbig.example:junit-5-lifecycle-callbacks >---------- [INFO] Building junit-5-lifecycle-callbacks 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-lifecycle-callbacks --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-lifecycle-callbacks\src\main\resources [INFO] [INFO] --- compiler:3.13.0:compile (default-compile) @ junit-5-lifecycle-callbacks --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-lifecycle-callbacks --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-lifecycle-callbacks\src\test\resources [INFO] [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ junit-5-lifecycle-callbacks --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-lifecycle-callbacks --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- DB: Starting connection pool DB: Clearing tables for test: loadUserData() Test: User data loaded Performance: loadUserData() took 124ms DB: Clearing tables for test: processOrder() Test: Order processed Performance: processOrder() took 59ms DB: Closing connection pool [INFO] +--com.logicbig.example.PracticalLifecycleTest - 0.244 ss [INFO] | +-- [OK] loadUserData - 0.173 ss [INFO] | '-- [OK] processOrder - 0.060 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.335 s [INFO] Finished at: 2025-12-31T14:34:37+08:00 [INFO] ------------------------------------------------------------------------
The output confirms that the extensions successfully shared data using the JUnit Store API. The DatabaseExtension managed the high-level lifecycle, while the PerformanceExtension calculated the precise duration of the test method. This separation of concerns illustrates how extensions keep test classes clean by moving infrastructure logic into reusable components.
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.0.0 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 5.0.0
- 5.0.1
- 5.0.2
- 5.0.3
- 5.1.0
- 5.1.1
- 5.2.0
- 5.3.0
- 5.3.1
- 5.3.2
- 5.4.0
- 5.4.1
- 5.4.2
- 5.5.0
- 5.5.1
- 5.5.2
- 5.6.0
- 5.6.1
- 5.6.2
- 5.6.3
- 5.7.0
- 5.7.1
- 5.7.2
- 5.8.0
- 5.8.1
- 5.8.2
- 5.9.0
- 5.9.1
- 5.9.2
- 5.9.3
- 5.10.0
- 5.10.1
- 5.10.2
- 5.10.3
- 5.10.4
- 5.10.5
- 5.11.0
- 5.11.1
- 5.11.2
- 5.11.3
- 5.11.4
- 5.12.0
- 5.12.1
- 5.12.2
- 5.13.0
- 5.13.1
- 5.13.2
- 5.13.3
- 5.13.4
- 5.14.0
- 5.14.1
- 6.0.0
- 6.0.1
Versions in green have been tested.
- JDK 25
- Maven 3.9.11
|