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 ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.11.0 - 6.0.1 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
|