The JUnit 5 extension model provides the ExecutableInvoker interface for programmatic method execution through the ExtensionContext. This enables extensions to invoke methods or constructors of test classes using JUnit's native invocation pipeline, ensuring parameter resolution, extension participation, and correct lifecycle behavior.
What is ExecutableInvoker?
Definition of ExecutableInvokerVersion: 6.0.1 package org.junit.jupiter.api.extension;
@API(status = STABLE, since = "5.11")
public interface ExecutableInvoker {
default Object invoke(Method method); 1
Object invoke(Method method, 2
@Nullable Object target);
default <T> T invoke(Constructor<T> constructor); 3
<T> T invoke(Constructor<T> constructor, 4
@Nullable Object outerInstance);
}
ExecutableInvoker is a service interface that provides methods for programmatically invoking constructors and methods. When obtained via ExtensionContext.getExecutableInvoker(), it uses JUnit's execution engine, which ensures proper parameter resolution through registered ParameterResolver extensions and respects the test lifecycle.
ExtensionContext Method to get ExecutableInvoker
ExecutableInvoker getExecutableInvoker()
Returns the ExecutableInvoker for programmatic method and constructor invocation. This invoker uses JUnit's execution pipeline, ensuring proper parameter resolution and extension participation.
Need for Programmatic Execution
Extensions sometimes need to execute test methods or helper methods programmatically—whether to implement retry logic, enforce policies, or create custom test orchestration. Direct reflection calls bypass JUnit's extension mechanism, while ExecutableInvoker ensures the full extension pipeline participates in the invocation.
ExecutableInvoker vs Invocation.proceed()
We have seen Invocation.proceed() in action here. Understanding when to use Invocation.proceed() and ExecutableInvoker.invoke() is crucial for extension design:
invocation.proceed(): Continues the current invocation chain that JUnit has already constructed. Use when wrapping execution, adding behavior before/after, or not changing execution flow (e.g., timing, logging, transaction boundaries).
ExecutableInvoker.invoke(): Performs a fresh invocation using JUnit's execution engine. Use when controlling execution, skipping execution entirely, invoking conditionally, or invoking multiple times (e.g., retry extensions, policy enforcement, test gating).
Parameter Resolution Requirement
As seen in the above ExecutableInvoker source code, you cannot directly pass arbitrary argument values to ExecutableInvoker.invoke(). All parameters are resolved exclusively through JUnit's ParameterResolver Extension mechanism. This ensures consistency with normal test execution and allows extensions to participate in parameter resolution.
Example
Policy Enforcement Extension
This example demonstrates a policy enforcement extension that intercepts test execution to verify business policies before allowing the actual test to run. The extension uses ExecutableInvoker to programmatically invoke a policy verification method, which receives parameters resolved by a custom ParameterResolver.
Invocation Interceptor Extension
package com.logicbig.example;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.*;
import java.lang.reflect.Method;
public class PolicyInvocationExtension implements InvocationInterceptor {
private static final ExtensionContext.Namespace NS =
ExtensionContext.Namespace.create(PolicyInvocationExtension.class);
@Override
public void interceptTestMethod(
Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext context) throws Throwable {
System.out.println("\n=== POLICY ENFORCEMENT ===");
System.out.println("Test: " + context.getDisplayName());
// Store custom value for ParameterResolver
String policyId = "POLICY-" + System.currentTimeMillis();
context.getStore(NS).put("policyId", policyId);
System.out.println("[POLICY] Generated policy ID: " + policyId);
Object instance = context.getRequiredTestInstance();
// Find the policy verification method
Method policyMethod =
context.getRequiredTestClass()
.getDeclaredMethod("verifyPolicy",
TestInfo.class,
String.class);
System.out.println("[INVOKE] Using ExecutableInvoker to call verifyPolicy");
System.out.println("[INVOKE] Method: " + policyMethod.getName());
// Correct invocation using ExecutableInvoker (no ExtensionContext argument)
// Parameters will be resolved by registered ParameterResolvers
context.getExecutableInvoker().invoke(policyMethod, instance);
System.out.println("[POLICY] Policy verification successful");
System.out.println("[PROCEED] Continuing with normal test execution");
// Continue with normal test execution
invocation.proceed();
}
}
Custom Annotation for passing policy id
package com.logicbig.example;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PolicyId {
}
Parameter Resolver Extension
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
import java.lang.reflect.Parameter;
public class PolicyIdParameterResolver implements ParameterResolver {
private static final ExtensionContext.Namespace NS =
ExtensionContext.Namespace.create(PolicyInvocationExtension.class);
@Override
public boolean supportsParameter(
ParameterContext pc,
ExtensionContext ec) {
return pc.isAnnotated(PolicyId.class)
&& pc.getParameter().getType() == String.class;
}
@Override
public Object resolveParameter(
ParameterContext pc,
ExtensionContext ec) {
return ec.getStore(NS)
.get("policyId", String.class);
}
}
Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith({PolicyInvocationExtension.class, PolicyIdParameterResolver.class})
public class PolicyDrivenTest {
// This method will be invoked by the extension using ExecutableInvoker
void verifyPolicy(TestInfo testInfo, @PolicyId String id) {
System.out.println("[VERIFY] Policy " + id +
" verified for " + testInfo.getDisplayName());
// Simulate policy validation logic
if (!id.startsWith("POLICY-")) {
throw new SecurityException("Invalid policy ID format: " + id);
}
System.out.println("[VERIFY] Policy validation passed");
}
@Test
void businessTest() {
System.out.println("[TEST] Executing actual business test logic");
// Actual test logic would go here
int result = processBusinessLogic(10, 20);
System.out.println("[TEST] Business logic result: " + result);
}
@Test
void anotherBusinessTest() {
System.out.println("[TEST] Executing another business test");
String data = transformData("input");
System.out.println("[TEST] Transformed data: " + data);
}
private int processBusinessLogic(int a, int b) {
return a * b; // Simulated business logic
}
private String transformData(String input) {
return input.toUpperCase() + "-processed";
}
}
Output$ mvn test -Dtest=PolicyDrivenTest [INFO] Scanning for projects... [INFO] [INFO] -----< com.logicbig.example:junit-5-extension-executable-invoker >------ [INFO] Building junit-5-extension-executable-invoker 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-executable-invoker --- [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-executable-invoker\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-executable-invoker --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-executable-invoker --- [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-executable-invoker\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-executable-invoker --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-executable-invoker --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] -------------------------------------------------------
=== POLICY ENFORCEMENT === Test: businessTest() [POLICY] Generated policy ID: POLICY-1767423693970 [INVOKE] Using ExecutableInvoker to call verifyPolicy [INVOKE] Method: verifyPolicy [VERIFY] Policy POLICY-1767423693970 verified for businessTest() [VERIFY] Policy validation passed [POLICY] Policy verification successful [PROCEED] Continuing with normal test execution [TEST] Executing actual business test logic [TEST] Business logic result: 200
=== POLICY ENFORCEMENT === Test: anotherBusinessTest() [POLICY] Generated policy ID: POLICY-1767423694026 [INVOKE] Using ExecutableInvoker to call verifyPolicy [INVOKE] Method: verifyPolicy [VERIFY] Policy POLICY-1767423694026 verified for anotherBusinessTest() [VERIFY] Policy validation passed [POLICY] Policy verification successful [PROCEED] Continuing with normal test execution [TEST] Executing another business test [TEST] Transformed data: INPUT-processed [INFO] +--com.logicbig.example.PolicyDrivenTest - 0.174 ss [INFO] | +-- [OK] businessTest - 0.124 ss [INFO] | '-- [OK] anotherBusinessTest - 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.375 s [INFO] Finished at: 2026-01-03T15:01:34+08:00 [INFO] ------------------------------------------------------------------------
Additional Test Examples
package com.logicbig.example;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.opentest4j.TestAbortedException;
@ExtendWith({PolicyInvocationExtension.class, PolicyIdParameterResolver.class})
public class PolicyDrivenTest2 {
void verifyPolicy(TestInfo testInfo, @PolicyId String id) {
System.out.println("[VERIFY] ExecutableInvokerTest policy verification");
System.out.println("[VERIFY] Test: " + testInfo.getDisplayName());
System.out.println("[VERIFY] Policy ID: " + id);
// Additional validation logic
if (testInfo.getTags().contains("skip")) {
System.out.println("[VERIFY] Test marked for skipping");
throw new TestAbortedException("Test should be skipped based on policy");
}
}
@Test
void normalTest() {
System.out.println("[TEST] Executing normal test");
// This test will execute normally after policy verification
}
@Test
@Tag("skip")
void testWithComplexLogic() {
System.out.println("[TEST] Executing test with complex logic");
}
}
Output$ mvn test -Dtest=PolicyDrivenTest2 [INFO] Scanning for projects... [INFO] [INFO] -----< com.logicbig.example:junit-5-extension-executable-invoker >------ [INFO] Building junit-5-extension-executable-invoker 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-executable-invoker --- [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-executable-invoker\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-executable-invoker --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-executable-invoker --- [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-executable-invoker\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-executable-invoker --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-executable-invoker --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] -------------------------------------------------------
=== POLICY ENFORCEMENT === Test: testWithComplexLogic() [POLICY] Generated policy ID: POLICY-1767423176344 [INVOKE] Using ExecutableInvoker to call verifyPolicy [INVOKE] Method: verifyPolicy [VERIFY] ExecutableInvokerTest policy verification [VERIFY] Test: testWithComplexLogic() [VERIFY] Policy ID: POLICY-1767423176344 [VERIFY] Test marked for skipping
=== POLICY ENFORCEMENT === Test: normalTest() [POLICY] Generated policy ID: POLICY-1767423176401 [INVOKE] Using ExecutableInvoker to call verifyPolicy [INVOKE] Method: verifyPolicy [VERIFY] ExecutableInvokerTest policy verification [VERIFY] Test: normalTest() [VERIFY] Policy ID: POLICY-1767423176401 [POLICY] Policy verification successful [PROCEED] Continuing with normal test execution [TEST] Executing normal test [INFO] +--com.logicbig.example.PolicyDrivenTest2 - 0.150 ss [INFO] | +-- [??] testWithComplexLogic - 0 ss [INFO] | '-- [OK] normalTest - 0.004 ss [INFO] [INFO] Results: [INFO] [WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.666 s [INFO] Finished at: 2026-01-03T14:52:56+08:00 [INFO] ------------------------------------------------------------------------
Conclusion
The output demonstrates how the PolicyInvocationExtension successfully uses ExecutableInvoker to programmatically invoke policy verification methods before allowing test execution. The extension stores a policy ID in the ExtensionContext.Store, which is then resolved by the PolicyIdParameterResolver when the verifyPolicy method is invoked. This pattern shows how extensions can enforce preconditions, validate business rules, or perform setup operations using JUnit's native invocation pipeline. The use of ExecutableInvoker ensures that all registered parameter resolvers participate in the invocation, maintaining consistency with normal test execution while enabling sophisticated extension behaviors like policy enforcement and conditional test execution.
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
|