The JUnit 5 extension model provides access to external configuration parameters through the ExtensionContext interface. This enables extensions to read configuration values from various sources like system properties, configuration files, or environment variables, making extensions configurable without code changes.
Need for Configuration Parameters
Extensions often need to be configurable for different environments or use cases. Hardcoding values limits reusability, while configuration parameters allow extensions to adapt to different testing scenarios. External configuration enables runtime customization without modifying extension code.
ExtensionContext Methods for Configuration Parameters
Optional<String> getConfigurationParameter(String key)
Retrieves a configuration parameter value as a String by its key. Returns Optional.empty() if the parameter is not found. This is the basic method for accessing string-based configuration.
<T> Optional<T> getConfigurationParameter(String key,
Function<? super String, ? extends @Nullable T> transformer)
Retrieves a configuration parameter and applies a transformation function to convert it to the desired type. Useful for parsing strings into numbers, booleans, enums, or custom types. Returns Optional.empty() if transformation fails.
Configuration Sources
Configuration parameters can be set through various sources with this priority order:
- JVM System Properties: Standard
-Dkey=value (e.g., -Dretry.maxAttempts=3)
- Configuration Files:
junit-platform.properties in classpath with simple key=value format
Example: Configurable Retry Extension
This example demonstrates a configurable retry extension that reads its behavior from external configuration. The extension reads maximum retry attempts, delay between retries, and retryable exception patterns from configuration parameters, showing how extensions can be customized without code changes.
Extension Implementation
package com.logicbig.example;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class ConfigurableRetryExtension implements BeforeEachCallback, AfterTestExecutionCallback {
private static final String MAX_ATTEMPTS_KEY = "retry.maxAttempts";
private static final String DELAY_KEY = "retry.delay";
private static final String RETRY_ON_KEY = "retry.retryOn";
private static final String ATTEMPT_KEY = "retry-attempt";
private static final Namespace NAMESPACE =
Namespace.create(ConfigurableRetryExtension.class);
@Override
public void beforeEach(ExtensionContext context) {
// Initialize attempt counter
context.getStore(NAMESPACE).put(ATTEMPT_KEY, 1);
// Read and log configuration
readConfiguration(context);
}
@Override
public void afterTestExecution(ExtensionContext context) {
Optional<Throwable> executionException = context.getExecutionException();
if (executionException.isPresent()) {
Throwable exception = executionException.get();
// Read configuration values
int maxAttempts = readMaxAttempts(context);
long delay = readDelay(context);
List<String> retryOnExceptions = readRetryOnExceptions(context);
// Check if exception should trigger retry
if (shouldRetry(exception, retryOnExceptions)) {
int currentAttempt = context.getStore(NAMESPACE).get(ATTEMPT_KEY, Integer.class);
if (currentAttempt < maxAttempts) {
System.out.println("[RETRY] Attempt " + currentAttempt +
" failed with " + exception.getClass().getSimpleName());
System.out.println("[RETRY] Retrying (max " + maxAttempts +
" attempts, delay " + delay + "ms)");
// Increment attempt counter
context.getStore(NAMESPACE).put(ATTEMPT_KEY, currentAttempt + 1);
// Simulate retry delay
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// In real implementation, you would re-execute the test
System.out.println("[RETRY] Would retry test here");
} else {
System.out.println("[RETRY] Max attempts (" + maxAttempts + ") reached");
}
} else {
System.out.println("[RETRY] Exception " + exception.getClass().getSimpleName() +
" not in retry list");
}
}
}
private void readConfiguration(ExtensionContext context) {
System.out.println("[CONFIG] Reading configuration parameters:");
// Read with transformation
Optional<Integer> maxAttempts = context.getConfigurationParameter(MAX_ATTEMPTS_KEY, Integer::valueOf);
maxAttempts.ifPresent(value ->
System.out.println(" " + MAX_ATTEMPTS_KEY + " = " + value)
);
Optional<Long> delay = context.getConfigurationParameter(DELAY_KEY, Long::valueOf);
delay.ifPresent(value ->
System.out.println(" " + DELAY_KEY + " = " + value + "ms")
);
Optional<List<String>> retryOn =
context.getConfigurationParameter(RETRY_ON_KEY,
value -> Arrays.asList(value.split(",")));
retryOn.ifPresent(list ->
System.out.println(" " + RETRY_ON_KEY + " = " + list)
);
// Show missing configuration
if (maxAttempts.isEmpty()) {
System.out.println(" " + MAX_ATTEMPTS_KEY + " = (not set, using default: 2)");
}
if (delay.isEmpty()) {
System.out.println(" " + DELAY_KEY + " = (not set, using default: 100ms)");
}
if (retryOn.isEmpty()) {
System.out.println(" " + RETRY_ON_KEY + " = (not set, using default: [])");
}
}
private int readMaxAttempts(ExtensionContext context) {
return context.getConfigurationParameter(MAX_ATTEMPTS_KEY, Integer::valueOf)
.orElse(2); // Default value
}
private long readDelay(ExtensionContext context) {
return context.getConfigurationParameter(DELAY_KEY, Long::valueOf)
.orElseThrow(() -> new RuntimeException(
"'%s' not provided".formatted(DELAY_KEY)));
}
private List<String> readRetryOnExceptions(ExtensionContext context) {
return context.getConfigurationParameter(RETRY_ON_KEY,
value -> Arrays.asList(value.split(",")))
.orElse(Collections.emptyList());
}
private boolean shouldRetry(Throwable exception,
List<String> retryOnExceptions) {
String exceptionName = exception.getClass().getSimpleName();
return retryOnExceptions.stream()
.anyMatch(exceptionName::contains);
}
}
Test Class Using the Extension
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ConfigurableRetryExtension.class)
public class ConfigurationParameterTest {
@Test
void successfulTest() {
System.out.println("[TEST] Executing successful test");
// This test should pass
int result = 10 + 5;
System.out.println("[TEST] Result: " + result);
}
@Test
void testWithTimeoutException() {
System.out.println("[TEST] Executing test that throws TimeoutException");
// Simulate timeout
throw new RuntimeException("Connection timeout after 5000ms");
}
@Test
void testWithNetworkException() {
System.out.println("[TEST] Executing test that throws NetworkException");
// Simulate network error
throw new RuntimeException("Network unreachable");
}
@Test
void testWithValidationException() {
System.out.println("[TEST] Executing test that throws ValidationException");
// This exception type is not in retry configuration
throw new RuntimeException("Validation failed: invalid input");
}
}
Output$ mvn test -Dtest=ConfigurationParameterTest -Dretry.maxAttempts=3 -Dretry.delay=200 -Dretry.retryOn=TimeoutException,NetworkException [INFO] Scanning for projects... [INFO] [INFO] --< com.logicbig.example:junit-5-extension-configuration-parameters >--- [INFO] Building junit-5-extension-configuration-parameters 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-configuration-parameters --- [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-configuration-parameters\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-extension-configuration-parameters --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-configuration-parameters --- [INFO] Copying 1 resource from src\test\resources to target\test-classes [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-extension-configuration-parameters --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-configuration-parameters --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [CONFIG] Reading configuration parameters: retry.maxAttempts = 3 retry.delay = 200ms retry.retryOn = [TimeoutException, NetworkException] [TEST] Executing test that throws TimeoutException [RETRY] Exception RuntimeException not in retry list [CONFIG] Reading configuration parameters: retry.maxAttempts = 3 retry.delay = 200ms retry.retryOn = [TimeoutException, NetworkException] [TEST] Executing successful test [TEST] Result: 15 [CONFIG] Reading configuration parameters: retry.maxAttempts = 3 retry.delay = 200ms retry.retryOn = [TimeoutException, NetworkException] [TEST] Executing test that throws ValidationException [RETRY] Exception RuntimeException not in retry list [CONFIG] Reading configuration parameters: retry.maxAttempts = 3 retry.delay = 200ms retry.retryOn = [TimeoutException, NetworkException] [TEST] Executing test that throws NetworkException [RETRY] Exception RuntimeException not in retry list [INFO] +--com.logicbig.example.ConfigurationParameterTest - 0.120 ss [INFO] | +-- [XX] testWithTimeoutException - 0.065 ss [INFO] | +-- [OK] successfulTest - 0.019 ss [INFO] | +-- [XX] testWithValidationException - 0.002 ss [INFO] | '-- [XX] testWithNetworkException - 0.003 ss [INFO] [INFO] Results: [INFO] [ERROR] Errors: [ERROR] ConfigurationParameterTest.testWithNetworkException:28 Runtime Network unreachable [ERROR] ConfigurationParameterTest.testWithTimeoutException:21 Runtime Connection timeout after 5000ms [ERROR] ConfigurationParameterTest.testWithValidationException:35 Runtime Validation failed: invalid input [INFO] [ERROR] Tests run: 4, Failures: 0, Errors: 3, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.353 s [INFO] Finished at: 2026-01-04T23:15:39+08:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.0:test (default-test) on project junit-5-extension-configuration-parameters: [ERROR] [ERROR] Please refer to D:\example-projects\junit-5\junit-5-extension-context\junit-5-extension-configuration-parameters\target\surefire-reports for the individual test results. [ERROR] Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream. [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
The output demonstrates how the ConfigurableRetryExtension successfully reads and applies configuration parameters.
Providing Configuration Parameters
Configuration parameters can be provided through multiple sources. In the example above, parameters are set via Maven command line:
mvn test -Dtest=ConfigurationParameterTest \
-Dretry.maxAttempts=3 \
-Dretry.delay=200 \
-Dretry.retryOn=TimeoutException,NetworkException
Alternative Configuration Methods
Configuration parameters can also be set through:
1. JUnit Platform Properties File
Create src/test/resources/junit-platform.properties:
retry.maxAttempts=3
retry.delay=200
retry.retryOn=TimeoutException,NetworkException
2. System Properties in pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
.........
<configuration>
<systemPropertyVariables>
<retry.maxAttempts>3</retry.maxAttempts>
<retry.delay>200</retry.delay>
<retry.retryOn>TimeoutException,NetworkException</retry.retryOn>
</systemPropertyVariables>
..................
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.7.0 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 5.7.0
- 5.7.1
- 5.7.2
- 5.8.0
- 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
|