Close

JUnit 5 - ExtensionContext - Programmatic Constructor Invocation with ExecutableInvoker

[Last Updated: Jan 3, 2026]

As we saw in the last example, the JUnit 5 extension model provides the ExecutableInvoker interface for programmatic method invocation. In this tutorial we will see how to invoke constructor using ExecutableInvoker.

What is ExecutableInvoker for Constructors?

ExecutableInvoker provides following overloaded invoke() methods for constructors. When used in TestInstanceFactory extensions, it allows custom test instance creation while ensuring proper parameter resolution through registered ParameterResolver extensions.

default <T> T invoke(Constructor<T> constructor)
<T> T invoke(Constructor<T> constructor,
             @Nullable Object outerInstance);

Here outerInstance is the enclosing instance required when invoking a non-static inner class constructor.

ExtensionContext method to get ExecutableInvoker

ExecutableInvoker getExecutableInvoker()

Returns the ExecutableInvoker for programmatic constructor and method invocation. When invoking constructors, it uses JUnit's execution pipeline to resolve constructor parameters.

When to Invoke a Constructor Programmatically

  • Custom Test Instance Creation: Inject custom values or generate IDs before instance creation
  • Inner Classes: Provide outer instance explicitly for inner class test instances
  • Dynamic Test Instances: Create multiple instances with different constructor arguments
  • Advanced DI Frameworks: Control instantiation while using JUnit's parameter resolution

Parameter Resolution Requirement

You cannot directly pass arbitrary argument values to ExecutableInvoker.invoke() for constructors. All constructor parameters are resolved exclusively through JUnit's ParameterResolver mechanism, ensuring consistency with normal test instantiation.

Example: Dynamic Order Test Factory

We have already seen a TestInstanceFactory example here, but that approach is not the standard or recommended one. In that example, we used reflection to invoke the constructor directly. Although this works, it violates Jupiter’s extension contract because it bypasses JUnit’s native invocation pipeline, including parameter resolution and extension participation.

This example demonstrates a TestInstanceFactory that creates test instances with dynamically generated order IDs. The factory uses ExecutableInvoker to invoke the test class constructor, while a ParameterResolver provides the generated order ID as a constructor parameter.

Test Instance Factory Extension

package com.logicbig.example;

import org.junit.jupiter.api.extension.*;
import java.lang.reflect.Constructor;
import java.util.UUID;

public class OrderTestInstanceFactory implements TestInstanceFactory {

    @Override
    public Object createTestInstance(
            TestInstanceFactoryContext factoryContext,
            ExtensionContext extensionContext) {

        Class<?> testClass = factoryContext.getTestClass();
        System.out.println("[FACTORY] Creating test instance for: " + testClass.getSimpleName());

        // Find the constructor with @OrderId parameter
        Constructor<?> constructor = null;
        for (Constructor<?> c : testClass.getDeclaredConstructors()) {
            if (c.getParameterCount() == 1 && 
                c.getParameters()[0].isAnnotationPresent(OrderId.class)) {
                constructor = c;
                break;
            }
        }

        if (constructor == null) {
            throw new IllegalStateException(
                "No constructor with @OrderId parameter found in " + testClass.getSimpleName());
        }

        System.out.println("[FACTORY] Using constructor: " + constructor);

        // Generate dynamic tracking information
        String instanceId = UUID.randomUUID().toString().substring(0, 8);
        System.out.println("[FACTORY] Generated instance ID: " + instanceId);

        // Store tracking info in ExtensionContext for later use
        extensionContext.getStore(ExtensionContext.Namespace.GLOBAL)
                        .put("instanceId", instanceId);

        // Use ExecutableInvoker to create the instance
        // Parameters will be resolved by registered ParameterResolvers
        System.out.println("[FACTORY] Invoking constructor via ExecutableInvoker");
        Object instance = extensionContext.getExecutableInvoker()
                .invoke(constructor, null);  // null because top-level class

        System.out.println("[FACTORY] Instance created successfully");
        System.out.println("[FACTORY] Instance hash: " + System.identityHashCode(instance));

        return instance;
    }
}

Parameter Resolver Extension

package com.logicbig.example;

import org.junit.jupiter.api.extension.*;

import java.util.UUID;

public class OrderIdParameterResolver implements
        ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
        return pc.isAnnotated(OrderId.class) && pc.getParameter().getType() == String.class;
    }

    @Override
    public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
        // Generate dynamic value (UUID, timestamp, etc.)
        return "ORDER-"+UUID.randomUUID().toString();
    }
}

Custom Annotation

package com.logicbig.example;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface OrderId {}

Test Class

package com.logicbig.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith({
    OrderTestInstanceFactory.class,
    OrderIdParameterResolver.class
})
public class OrderTest {
    private final String orderId;

    public OrderTest(@OrderId String orderId) {
        this.orderId = orderId;
        System.out.println("[CONSTRUCTOR] OrderTest instance created");
        System.out.println("[CONSTRUCTOR] Order ID: " + orderId);
        System.out.println("[CONSTRUCTOR] Instance: " + this.getClass().getSimpleName() + 
                         "@" + System.identityHashCode(this));
    }

    @Test
    void testOrderCreation() {
        System.out.println("[TEST] Running testOrderCreation with orderId = " + orderId);

        // Simulate order validation logic
        if (!orderId.startsWith("ORDER-")) {
            throw new IllegalArgumentException("Invalid order ID format: " + orderId);
        }

        System.out.println("[TEST] Order validation passed");

        // Simulate business logic
        String orderStatus = processOrder(orderId);
        System.out.println("[TEST] Order status: " + orderStatus);
    }

    @Test
    void testOrderProcessing() {
        System.out.println("[TEST] Running testOrderProcessing with orderId = " + orderId);

        // Demonstrate that the same instance is used for both tests in PER_CLASS lifecycle
        System.out.println("[TEST] Instance reference: " + System.identityHashCode(this));

        // Simulate order processing logic
        double total = calculateOrderTotal(2, 49.99);
        System.out.println("[TEST] Order total: $" + total);
    }

    private String processOrder(String orderId) {
        return "PROCESSED-" + orderId;
    }

    private double calculateOrderTotal(int quantity, double price) {
        return quantity * price;
    }
}

Output

$ mvn test -Dtest=OrderTest
[INFO] Scanning for projects...
[INFO]
[INFO] ---< com.logicbig.example:junit-5-extension-constructor-invocation >----
[INFO] Building junit-5-extension-constructor-invocation 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-constructor-invocation ---
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-constructor-invocation\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-constructor-invocation ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-constructor-invocation ---
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-constructor-invocation\src\test\resources
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-constructor-invocation ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-constructor-invocation ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[FACTORY] Creating test instance for: OrderTest
[FACTORY] Using constructor: public com.logicbig.example.OrderTest(java.lang.String)
[FACTORY] Generated instance ID: a938df14
[FACTORY] Invoking constructor via ExecutableInvoker
[CONSTRUCTOR] OrderTest instance created
[CONSTRUCTOR] Order ID: ORDER-93fa6719-7da0-441b-853d-0e86cf89c9d0
[CONSTRUCTOR] Instance: OrderTest@1676605578
[FACTORY] Instance created successfully
[FACTORY] Instance hash: 1676605578
[TEST] Running testOrderProcessing with orderId = ORDER-93fa6719-7da0-441b-853d-0e86cf89c9d0
[TEST] Instance reference: 1676605578
[TEST] Order total: $99.98
[FACTORY] Creating test instance for: OrderTest
[FACTORY] Using constructor: public com.logicbig.example.OrderTest(java.lang.String)
[FACTORY] Generated instance ID: f1d26c36
[FACTORY] Invoking constructor via ExecutableInvoker
[CONSTRUCTOR] OrderTest instance created
[CONSTRUCTOR] Order ID: ORDER-b49f80bc-1e58-4df8-be5b-995da2d37d6d
[CONSTRUCTOR] Instance: OrderTest@2049051802
[FACTORY] Instance created successfully
[FACTORY] Instance hash: 2049051802
[TEST] Running testOrderCreation with orderId = ORDER-b49f80bc-1e58-4df8-be5b-995da2d37d6d
[TEST] Order validation passed
[TEST] Order status: PROCESSED-ORDER-b49f80bc-1e58-4df8-be5b-995da2d37d6d
[INFO] +--com.logicbig.example.OrderTest - 0.208 ss
[INFO] | +-- [OK] testOrderProcessing - 0.021 ss
[INFO] | '-- [OK] testOrderCreation - 0 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.076 s
[INFO] Finished at: 2026-01-03T16:08:41+08:00
[INFO] ------------------------------------------------------------------------

The output demonstrates how the OrderTestInstanceFactory successfully creates test instances using ExecutableInvoker for constructor invocation. Each test instance receives a unique, dynamically generated order ID through the constructor parameter resolved by the OrderIdParameterResolver. This pattern shows how extensions can control test instance creation while leveraging JUnit's parameter resolution mechanism. The factory ensures that each test run has distinct, traceable instances with proper dependency injection, enabling advanced testing scenarios like dynamic test data generation, isolated test instances with unique identifiers, and custom instantiation logic that integrates seamlessly with JUnit's extension ecosystem.

Example Project

Dependencies and Technologies Used:

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

    Version compatibilities of junit-jupiter-engine with this example:

    • 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 Constructor Invocation with ExecutableInvoker Select All Download
  • junit-5-extension-constructor-invocation
    • src
      • test
        • java
          • com
            • logicbig
              • example
                • OrderTest.java

    See Also