The JUnit 5 extension model provides a hierarchical view of test execution through the ExtensionContext interface. Understanding this hierarchy is essential for extensions that need to operate at specific levels of the test tree or share information between related test components.
Need for Hierarchy Navigation
Extensions often need to understand their position in the test execution tree to make context-aware decisions. For example, an extension might need to apply different behaviors for nested test classes versus top-level classes, or share configuration data between a test class and its nested test methods. Without proper hierarchy navigation, extensions would treat all test executions uniformly, missing opportunities for targeted optimizations and contextual configurations.
Navigation Methods
Optional<ExtensionContext> getParent()
Returns the parent context, allowing upward traversal of the test tree. Essential for extensions that need to access or modify parent-level state.
ExtensionContext getRoot()
Returns the root context of the test tree. Useful for extensions that need to access global configuration or establish baseline behavior.
List<Class<?>> getEnclosingTestClasses()
Returns the hierarchy of enclosing test classes from outermost to innermost. This is particularly useful for nested test classes where you need to know the complete class hierarchy.
Identification Methods
String getUniqueId()
Returns a technical identifier for the current context node. This ID follows a specific format and can be used for debugging, logging, or creating deterministic keys for storage.
String getDisplayName()
Returns a human-readable name for the current context. Ideal for reporting, logging, and user-facing messages where readability matters more than technical precision.
Understanding the Test Hierarchy
The test execution tree follows this typical structure:
- Root: The top-level container (engine or suite)
- Class Level: Test class contexts containing method contexts
- Method Level: Individual test method executions
- Nested Contexts: Additional levels for parameterized tests, dynamic tests, or nested classes
Each level provides appropriate context for extensions to operate at the right granularity. The getEnclosingTestClasses() method is especially valuable for understanding nested class relationships.
Use Cases
- Conditional Behavior: Apply different logging levels based on test depth or enclosing class hierarchy
- Data Sharing: Share setup data between parent and child contexts or across nested classes
- Resource Management: Initialize resources at appropriate class level in nested hierarchies
- Dynamic Configuration: Adjust timeout settings based on test nesting level or enclosing classes
- Hierarchical Test Data: Load different test data based on the complete class hierarchy
- Cross-Class Configuration: Apply configurations that span multiple levels of nested classes
Example
Extension Implementation
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
import java.util.List;
public class HierarchyAwareExtension implements BeforeEachCallback, BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) {
System.out.println("\n=== BEFORE ALL ===");
printContextInfo("Class Level", context);
}
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("\n=== BEFORE EACH ===");
printContextInfo("Method Level", context);
// Show parent hierarchy
showParentHierarchy(context);
// Show enclosing classes hierarchy
showEnclosingClasses(context);
// Show hierarchy depth
int depth = calculateHierarchyDepth(context);
System.out.println("Hierarchy Depth: " + depth);
}
private void printContextInfo(String phase, ExtensionContext context) {
System.out.println("Phase: " + phase);
System.out.println("Display Name: " + context.getDisplayName());
System.out.println("Unique ID: " + context.getUniqueId());
System.out.println("---");
}
private void showParentHierarchy(ExtensionContext context) {
System.out.println("Parent Hierarchy:");
System.out.println(" Current -> " + context.getDisplayName());
ExtensionContext current = context;
int level = 1;
while (current.getParent().isPresent()) {
current = current.getParent().get();
System.out.println(" Parent Level " + level + " -> " + current.getDisplayName());
level++;
}
System.out.println(" Root -> " + context.getRoot().getDisplayName());
}
private void showEnclosingClasses(ExtensionContext context) {
List<Class<?>> enclosingClasses = context.getEnclosingTestClasses();
if (!enclosingClasses.isEmpty()) {
System.out.println("Enclosing Classes (outermost to innermost):");
for (int i = 0; i < enclosingClasses.size(); i++) {
System.out.println(" Level " + i + ": " + enclosingClasses.get(i).getSimpleName());
}
} else {
System.out.println("No enclosing classes (top-level test)");
}
}
private int calculateHierarchyDepth(ExtensionContext context) {
int depth = 0;
ExtensionContext current = context;
while (current.getParent().isPresent()) {
depth++;
current = current.getParent().get();
}
return depth;
}
}
Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(HierarchyAwareExtension.class)
public class HierarchyExtensionTest {
@Test
void topLevelTest1() {
System.out.println("Executing topLevelTest1");
// Simulate some work
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Test
void topLevelTest2() {
System.out.println("Executing topLevelTest2");
// Simulate different work duration
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Output$ mvn test -Dtest=HierarchyExtensionTest [INFO] Scanning for projects... [INFO] [INFO] ------< com.logicbig.example:junit-5-extension-context-hierarchy >------ [INFO] Building junit-5-extension-context-hierarchy 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-context-hierarchy --- [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-extension-context\junit-5-extension-context-hierarchy\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-context-hierarchy --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-context-hierarchy --- [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-extension-context\junit-5-extension-context-hierarchy\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-context-hierarchy --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-context-hierarchy --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] -------------------------------------------------------
=== BEFORE ALL === Phase: Class Level Display Name: HierarchyExtensionTest Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.HierarchyExtensionTest] ---
=== BEFORE EACH === Phase: Method Level Display Name: topLevelTest1() Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.HierarchyExtensionTest]/[method:topLevelTest1()] --- Parent Hierarchy: Current -> topLevelTest1() Parent Level 1 -> HierarchyExtensionTest Parent Level 2 -> JUnit Jupiter Root -> JUnit Jupiter No enclosing classes (top-level test) Hierarchy Depth: 2 Executing topLevelTest1
=== BEFORE EACH === Phase: Method Level Display Name: topLevelTest2() Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.HierarchyExtensionTest]/[method:topLevelTest2()] --- Parent Hierarchy: Current -> topLevelTest2() Parent Level 1 -> HierarchyExtensionTest Parent Level 2 -> JUnit Jupiter Root -> JUnit Jupiter No enclosing classes (top-level test) Hierarchy Depth: 2 Executing topLevelTest2 [INFO] +--com.logicbig.example.HierarchyExtensionTest - 0.423 ss [INFO] | +-- [OK] topLevelTest1 - 0.169 ss [INFO] | '-- [OK] topLevelTest2 - 0.218 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.259 s [INFO] Finished at: 2026-01-02T16:36:41+08:00 [INFO] ------------------------------------------------------------------------
Nested Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(HierarchyAwareExtension.class)
public class NestedHierarchyTest {
@Test
void outerTest() {
System.out.println("Executing outerTest");
}
@Nested
class FirstNestedLevel {
@Test
void firstLevelTest() {
System.out.println("Executing firstLevelTest");
}
@Nested
class SecondNestedLevel {
@Test
void secondLevelTest() {
System.out.println("Executing secondLevelTest");
}
}
}
}
Output$ mvn test -Dtest=NestedHierarchyTest [INFO] Scanning for projects... [INFO] [INFO] ------< com.logicbig.example:junit-5-extension-context-hierarchy >------ [INFO] Building junit-5-extension-context-hierarchy 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-context-hierarchy --- [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-extension-context\junit-5-extension-context-hierarchy\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-context-hierarchy --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-context-hierarchy --- [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-extension-context\junit-5-extension-context-hierarchy\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-context-hierarchy --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-context-hierarchy --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] -------------------------------------------------------
=== BEFORE ALL === Phase: Class Level Display Name: NestedHierarchyTest Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest] ---
=== BEFORE EACH === Phase: Method Level Display Name: outerTest() Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest]/[method:outerTest()] --- Parent Hierarchy: Current -> outerTest() Parent Level 1 -> NestedHierarchyTest Parent Level 2 -> JUnit Jupiter Root -> JUnit Jupiter No enclosing classes (top-level test) Hierarchy Depth: 2 Executing outerTest
=== BEFORE ALL === Phase: Class Level Display Name: FirstNestedLevel Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest]/[nested-class:FirstNestedLevel] ---
=== BEFORE EACH === Phase: Method Level Display Name: firstLevelTest() Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest]/[nested-class:FirstNestedLevel]/[method:firstLevelTest()] --- Parent Hierarchy: Current -> firstLevelTest() Parent Level 1 -> FirstNestedLevel Parent Level 2 -> NestedHierarchyTest Parent Level 3 -> JUnit Jupiter Root -> JUnit Jupiter Enclosing Classes (outermost to innermost): Level 0: NestedHierarchyTest Hierarchy Depth: 3 Executing firstLevelTest
=== BEFORE ALL === Phase: Class Level Display Name: SecondNestedLevel Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest]/[nested-class:FirstNestedLevel]/[nested-class:SecondNestedLevel] ---
=== BEFORE EACH === Phase: Method Level Display Name: secondLevelTest() Unique ID: [engine:junit-jupiter]/[class:com.logicbig.example.NestedHierarchyTest]/[nested-class:FirstNestedLevel]/[nested-class:SecondNestedLevel]/[method:secondLevelTest()] --- Parent Hierarchy: Current -> secondLevelTest() Parent Level 1 -> SecondNestedLevel Parent Level 2 -> FirstNestedLevel Parent Level 3 -> NestedHierarchyTest Parent Level 4 -> JUnit Jupiter Root -> JUnit Jupiter Enclosing Classes (outermost to innermost): Level 0: NestedHierarchyTest Level 1: FirstNestedLevel Hierarchy Depth: 4 Executing secondLevelTest [INFO] +--com.logicbig.example.NestedHierarchyTest - 0.108 ss [INFO] | '-- [OK] outerTest - 0.048 ss [INFO] +--.--com.logicbig.example.NestedHierarchyTest$FirstNestedLevel - 0.025 ss [INFO] | | '-- [OK] firstLevelTest - 0.008 ss [INFO] | '-----com.logicbig.example.NestedHierarchyTest$FirstNestedLevel$SecondNestedLevel - 0.008 ss [INFO] | '-- [OK] secondLevelTest - 0.002 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.251 s [INFO] Finished at: 2026-01-02T16:36:55+08:00 [INFO] ------------------------------------------------------------------------
Conclusion
The output demonstrates how the HierarchyAwareExtension effectively navigates and identifies different levels of the test execution tree. The extension correctly identifies the root context, distinguishes between class-level and method-level executions, and properly handles nested test structures using both parent navigation and enclosing class hierarchy. The getEnclosingTestClasses() method reveals the complete class hierarchy for nested tests, while getUniqueId() shows the technical relationships between contexts. This comprehensive hierarchical awareness enables extensions to make intelligent decisions based on execution scope, such as applying different timeout policies at different levels, sharing resources appropriately within nested class structures, and loading context-specific configurations based on the complete test hierarchy.
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.12.1 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 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
|