Close

JUnit 5 - ExtensionContext - Metadata & Test Discovery

[Last Updated: Jan 3, 2026]

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 Project

Dependencies and Technologies Used:

  • junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
     Version Compatibility: 5.8.1 - 6.0.1Version List
    ×

    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

JUnit 5 - Extension Context Metadata & Test Discovery Select All Download
  • junit-5-extension-metadata-discovery
    • src
      • test
        • java
          • com
            • logicbig
              • example
                • MetadataExtensionTest.java

    See Also