The JUnit 5 extension model provides rich metadata about test executions through the ExtensionContext interface. Understanding this metadata is crucial for extensions that need to make decisions based on test annotations, tags, or execution characteristics.
Need for Metadata Discovery
Extensions often need to adapt their behavior based on what is being executed and how it is configured. Without access to test metadata, extensions would apply uniform behavior to all tests, missing opportunities for conditional logic, resource optimization, and context-aware operations. Metadata discovery enables extensions to respond intelligently to test configurations.
Element Discovery Methods
Optional<AnnotatedElement> getElement()
Returns the AnnotatedElement (typically a Class or Method) that triggered the extension. Essential for inspecting custom annotations and understanding the test structure.
Optional<Class<?>> getTestClass()
Returns the test class being executed. Useful when you need the class object rather than just its annotations.
Class<?> getRequiredTestClass()
Returns the test class being executed, throwing IllegalStateException if no test class is available. Useful when your extension always requires a test class context and wants to fail fast with a clear error message.
Optional<Method> getTestMethod()
Returns the test method being executed. Provides direct access to method metadata and parameters.
Method getRequiredTestMethod()
Returns the test method being executed, throwing IllegalStateException if no test method is available. Useful for method-level extensions that require method context.
Test Configuration Methods
Set<String> getTags()
Retrieves all tags associated with the current test. Enables conditional logic based on test categorization (e.g., "slow", "integration", "database").
ExecutionMode getExecutionMode()
Returns whether the test is running in CONCURRENT or SAME_THREAD mode. Crucial for managing thread-safe resources and synchronization.
Annotation Processing
Custom annotations provide a powerful way to configure extension behavior. By inspecting annotations via getElement(), extensions can adapt to specific test requirements without complex configuration systems. For example, an @AuditLog annotation can enable detailed audit trail logging for security-sensitive tests.
Tag-Based Conditional Logic
Tags allow tests to be categorized for different purposes. Extensions can use getTags() to apply different behaviors for different test categories, such as extended timeouts for slow tests or database setup for integration tests.
Execution Mode Awareness
With parallel test execution becoming common, extensions must be aware of getExecutionMode() to properly manage shared resources. Thread-local storage or synchronization may be necessary in CONCURRENT mode.
Example
Extension Implementation
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.lang.reflect.Method;
import java.util.Set;
public class MetadataAwareExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("\n=== TEST METADATA ANALYSIS ===");
// 1. Analyze the element (Class/Method) and its annotations
analyzeElement(context);
// 2. Check and process tags
processTags(context);
// 3. Check execution mode for thread safety
checkExecutionMode(context);
// 4. Apply configuration based on metadata
applyConfiguration(context);
}
private void analyzeElement(ExtensionContext context) {
System.out.println("Element Analysis:");
// Get the annotated element (method or class)
context.getElement().ifPresent(element -> {
System.out.println(" Element Type: " + element.getClass().getSimpleName());
System.out.println(" Element Name: " + element.toString());
// Check for custom annotations
if (element.isAnnotationPresent(AuditLog.class)) {
AuditLog annotation = element.getAnnotation(AuditLog.class);
System.out.println(" Found @AuditLog");
System.out.println(" Level: " + annotation.level());
System.out.println(" Category: " + annotation.category());
System.out.println(" Track Arguments: " + annotation.trackArguments());
}
});
// Get test class and method specifically
context.getTestClass().ifPresent(testClass -> {
System.out.println(" Test Class: " + testClass.getSimpleName());
});
//instead of getTestClass() You can use:
try {
Class<?> testClass = context.getRequiredTestClass();
System.out.println(" Required Test Class: " + testClass.getSimpleName());
} catch (IllegalStateException e) {
System.out.println(" No test class available: " + e.getMessage());
}
context.getTestMethod().ifPresent(method -> {
System.out.println(" Test Method: " + method.getName());
System.out.println(" Return Type: " + method.getReturnType().getSimpleName());
System.out.println(" Parameter Count: " + method.getParameterCount());
});
//instead of getTestMethod() You can use:
try {
Method method = context.getRequiredTestMethod();
System.out.println(" Required Test Method: " + method.getName());
System.out.println(" Required TestMethod Return Type: " +
method.getReturnType().getSimpleName());
} catch (IllegalStateException e) {
System.out.println(" No test method available: " + e.getMessage());
}
}
private void processTags(ExtensionContext context) {
Set<String> tags = context.getTags();
System.out.println("Tag Analysis:");
if (tags.isEmpty()) {
System.out.println(" No tags found");
return;
}
System.out.println(" Tags: " + String.join(", ", tags));
// Apply conditional logic based on tags
if (tags.contains("slow")) {
System.out.println(" ⚠ SLOW TEST: Enabling extended monitoring");
}
if (tags.contains("integration")) {
System.out.println(" INTEGRATION TEST: Database setup required");
}
if (tags.contains("security")) {
System.out.println(" SECURITY TEST: Enabling audit logging");
}
}
private void checkExecutionMode(ExtensionContext context) {
ExecutionMode mode = context.getExecutionMode();
System.out.println("Execution Mode:");
System.out.println(" Mode: " + mode);
switch (mode) {
case SAME_THREAD:
System.out.println(" Single-threaded execution");
break;
case CONCURRENT:
System.out.println(" Concurrent execution - thread safety required");
System.out.println(" Using thread-local storage for test data");
break;
}
}
private void applyConfiguration(ExtensionContext context) {
System.out.println("Applied Configuration:");
// Enable audit logging if annotation is present
boolean auditEnabled = false;
String auditLevel = "INFO";
String auditCategory = "general";
context.getElement().ifPresent(element -> {
if (element.isAnnotationPresent(AuditLog.class)) {
AuditLog annotation = element.getAnnotation(AuditLog.class);
System.out.println(" Audit logging enabled");
System.out.println(" Log Level: " + annotation.level());
System.out.println(" Category: " + annotation.category());
if (annotation.trackArguments()) {
System.out.println(" Argument tracking: Enabled");
}
}
});
// Security enhancements for security-tagged tests
Set<String> tags = context.getTags();
if (tags.contains("security")) {
System.out.println(" Security enhancements enabled");
System.out.println(" Input validation: Strict mode");
System.out.println(" Log sanitization: Enabled");
}
// Thread safety measures for concurrent execution
if (context.getExecutionMode() == ExecutionMode.CONCURRENT) {
System.out.println(" Concurrent execution safeguards");
System.out.println(" Resource locking: Enabled");
System.out.println(" Test isolation: Per-thread");
}
System.out.println("---");
}
}
Custom Annotation
package com.logicbig.example;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
String level() default "INFO";
String category() default "general";
boolean trackArguments() default false;
}
Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MetadataAwareExtension.class)
public class MetadataExtensionTest {
@Test
@Tag("fast")
void fastTest() {
System.out.println("Executing fastTest");
}
@Test
@Tag("slow")
@Tag("integration")
@AuditLog(level = "DEBUG", category = "database")
void slowIntegrationTest() {
System.out.println("Executing slowIntegrationTest");
try {
Thread.sleep(100); // Simulate slow operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Test
@Tag("security")
@Tag("integration")
@AuditLog(level = "AUDIT", category = "security", trackArguments = true)
void securityIntegrationTest() {
System.out.println("Executing securityIntegrationTest");
// Simulate security check
String sensitiveData = "password123";
System.out.println("Processing sensitive operation");
}
@Test
@Tag("fast")
@AuditLog // Uses default values
void fastTestWithAudit() {
System.out.println("Executing fastTestWithAudit");
}
}
Output$ mvn compile test -Dtest=MetadataExtensionTest [INFO] Scanning for projects... [INFO] [INFO] -----< com.logicbig.example:junit-5-extension-metadata-discovery >------ [INFO] Building junit-5-extension-metadata-discovery 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-metadata-discovery --- [WARNING] Using platform encoding (UTF-8 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-metadata-discovery\src\main\resources [INFO] [INFO] --- compiler:3.14.1:compile (default-compile) @ junit-5-extension-metadata-discovery --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-metadata-discovery --- [WARNING] Using platform encoding (UTF-8 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-metadata-discovery\src\main\resources [INFO] [INFO] --- compiler:3.14.1:compile (default-compile) @ junit-5-extension-metadata-discovery --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-metadata-discovery --- [WARNING] Using platform encoding (UTF-8 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-metadata-discovery\src\test\resources [INFO] [INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ junit-5-extension-metadata-discovery --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 3 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.5.4:test (default-test) @ junit-5-extension-metadata-discovery --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] -------------------------------------------------------
=== TEST METADATA ANALYSIS === Element Analysis: Element Type: Method Element Name: void com.logicbig.example.MetadataExtensionTest.securityIntegrationTest() Found @AuditLog Level: AUDIT Category: security Track Arguments: true Test Class: MetadataExtensionTest Required Test Class: MetadataExtensionTest Test Method: securityIntegrationTest Return Type: void Parameter Count: 0 Required Test Method: securityIntegrationTest Required TestMethod Return Type: void Tag Analysis: Tags: security, integration INTEGRATION TEST: Database setup required SECURITY TEST: Enabling audit logging Execution Mode: Mode: SAME_THREAD Single-threaded execution Applied Configuration: Audit logging enabled Log Level: AUDIT Category: security Argument tracking: Enabled Security enhancements enabled Input validation: Strict mode Log sanitization: Enabled --- Executing securityIntegrationTest Processing sensitive operation
=== TEST METADATA ANALYSIS === Element Analysis: Element Type: Method Element Name: void com.logicbig.example.MetadataExtensionTest.fastTestWithAudit() Found @AuditLog Level: INFO Category: general Track Arguments: false Test Class: MetadataExtensionTest Required Test Class: MetadataExtensionTest Test Method: fastTestWithAudit Return Type: void Parameter Count: 0 Required Test Method: fastTestWithAudit Required TestMethod Return Type: void Tag Analysis: Tags: fast Execution Mode: Mode: SAME_THREAD Single-threaded execution Applied Configuration: Audit logging enabled Log Level: INFO Category: general --- Executing fastTestWithAudit
=== TEST METADATA ANALYSIS === Element Analysis: Element Type: Method Element Name: void com.logicbig.example.MetadataExtensionTest.fastTest() Test Class: MetadataExtensionTest Required Test Class: MetadataExtensionTest Test Method: fastTest Return Type: void Parameter Count: 0 Required Test Method: fastTest Required TestMethod Return Type: void Tag Analysis: Tags: fast Execution Mode: Mode: SAME_THREAD Single-threaded execution Applied Configuration: --- Executing fastTest
=== TEST METADATA ANALYSIS === Element Analysis: Element Type: Method Element Name: void com.logicbig.example.MetadataExtensionTest.slowIntegrationTest() Found @AuditLog Level: DEBUG Category: database Track Arguments: false Test Class: MetadataExtensionTest Required Test Class: MetadataExtensionTest Test Method: slowIntegrationTest Return Type: void Parameter Count: 0 Required Test Method: slowIntegrationTest Required TestMethod Return Type: void Tag Analysis: Tags: slow, integration ? SLOW TEST: Enabling extended monitoring INTEGRATION TEST: Database setup required Execution Mode: Mode: SAME_THREAD Single-threaded execution Applied Configuration: Audit logging enabled Log Level: DEBUG Category: database --- Executing slowIntegrationTest [INFO] +--com.logicbig.example.MetadataExtensionTest - 0.201 ss [INFO] | +-- [OK] securityIntegrationTest - 0.052 ss [INFO] | +-- [OK] fastTestWithAudit - 0.008 ss [INFO] | +-- [OK] fastTest - 0.003 ss [INFO] | '-- [OK] slowIntegrationTest - 0.110 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.844 s [INFO] Finished at: 2026-01-03T07:16:01+08:00 [INFO] ------------------------------------------------------------------------
The output demonstrates how the MetadataAwareExtension intelligently adapts its behavior based on test metadata. The extension correctly identifies custom annotations like @AuditLog and enables appropriate audit trail logging with configured log levels and categories. It recognizes test tags such as "security" and "integration" to apply specialized behaviors like enhanced security checks and database setup. The execution mode awareness ensures thread-safe operation in concurrent environments. By leveraging getElement(), getTags(), and getExecutionMode(), the extension provides context-aware behavior that enhances test execution without requiring manual configuration for each test scenario.
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.8.1 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 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
|