JUnit Platform Engine API - Core engine API for implementing custom test engines in the JUnit Platform ecosystem
—
Configuration system and hierarchical storage for test engines to manage parameters and test-scoped data. This system provides engines with access to configuration parameters and hierarchical data storage with automatic lifecycle management.
Interface for accessing configuration parameters provided to test engines during discovery and execution.
/**
* Configuration parameters for test engines, providing type-safe parameter access.
*/
public interface ConfigurationParameters {
/**
* Get a configuration parameter value.
* @param key the parameter key
* @return optional parameter value
*/
Optional<String> get(String key);
/**
* Get a boolean configuration parameter.
* @param key the parameter key
* @return optional boolean value (true/false)
*/
Optional<Boolean> getBoolean(String key);
/**
* Get a configuration parameter with custom transformation.
* @param key the parameter key
* @param transformer function to transform string value to desired type
* @return optional transformed value
*/
<T> Optional<T> get(String key, Function<String, T> transformer);
/**
* Get all configuration parameter keys.
* @return unmodifiable set of parameter keys
*/
Set<String> keySet();
/**
* Default configuration file name.
*/
String CONFIG_FILE_NAME = "junit-platform.properties";
}Usage Example:
@Override
public void execute(ExecutionRequest request) {
ConfigurationParameters config = request.getConfigurationParameters();
// Get string parameters
String environment = config.get("test.environment").orElse("development");
Optional<String> dbUrl = config.get("database.url");
// Get boolean parameters
boolean parallelExecution = config.getBoolean("junit.jupiter.execution.parallel.enabled")
.orElse(false);
boolean verboseLogging = config.getBoolean("logging.verbose").orElse(false);
// Get numeric parameters with transformation
int timeout = config.get("test.timeout.seconds", Integer::parseInt).orElse(30);
double retryDelay = config.get("retry.delay", Double::parseDouble).orElse(1.0);
// Get enum parameters
LogLevel logLevel = config.get("log.level", LogLevel::valueOf)
.orElse(LogLevel.INFO);
// List all available configuration keys
Set<String> allKeys = config.keySet();
System.out.println("Available configuration keys: " + allKeys);
}Configuration parameters wrapper that automatically applies a prefix to parameter keys, useful for engine-specific configuration.
/**
* Configuration parameters with automatic prefix handling for engine-specific configuration.
*/
public class PrefixedConfigurationParameters implements ConfigurationParameters {
/**
* Create prefixed configuration parameters.
* @param delegate the underlying configuration parameters
* @param prefix the prefix to apply to keys
*/
public PrefixedConfigurationParameters(ConfigurationParameters delegate, String prefix);
// All ConfigurationParameters methods automatically apply prefix
}Usage Example:
public class MyTestEngine implements TestEngine {
private static final String ENGINE_PREFIX = "myengine.";
@Override
public void execute(ExecutionRequest request) {
// Create engine-specific configuration
ConfigurationParameters engineConfig = new PrefixedConfigurationParameters(
request.getConfigurationParameters(), ENGINE_PREFIX
);
// Access engine-specific parameters without prefix
boolean enableFeature = engineConfig.getBoolean("feature.enabled").orElse(false);
// Actual key looked up: "myengine.feature.enabled"
int threadCount = engineConfig.get("thread.count", Integer::parseInt).orElse(1);
// Actual key looked up: "myengine.thread.count"
}
}Hierarchical, namespaced key-value store with automatic resource management and parent-child relationships for test-scoped data storage.
/**
* Hierarchical, namespaced key-value store with automatic resource management.
* @param N the namespace type
*/
public final class NamespacedHierarchicalStore<N> implements AutoCloseable {
/**
* Get a value from the store.
* @param namespace the namespace
* @param key the key
* @return the stored value, or null if not found
*/
public Object get(N namespace, Object key);
/**
* Get a typed value from the store.
* @param namespace the namespace
* @param key the key
* @param requiredType the required value type
* @return the stored value cast to the required type
* @throws ClassCastException if value cannot be cast to required type
*/
public <T> T get(N namespace, Object key, Class<T> requiredType);
/**
* Store a value in the store.
* @param namespace the namespace
* @param key the key
* @param value the value to store
* @return the previously stored value, or null if none
*/
public Object put(N namespace, Object key, Object value);
/**
* Remove a value from the store.
* @param namespace the namespace
* @param key the key
* @return the removed value, or null if not found
*/
public Object remove(N namespace, Object key);
/**
* Get a value or compute it if absent.
* @param namespace the namespace
* @param key the key
* @param defaultCreator function to create default value
* @return existing or newly computed value
*/
public <K, V> Object getOrComputeIfAbsent(N namespace, K key, Function<K, V> defaultCreator);
/**
* Create a child store with this store as parent.
* @return new child store
*/
public NamespacedHierarchicalStore<N> newChild();
/**
* Get the parent store.
* @return optional parent store
*/
public Optional<NamespacedHierarchicalStore<N>> getParent();
/**
* Check if this store has been closed.
* @return true if closed
*/
public boolean isClosed();
/**
* Close this store and execute all close actions.
* Automatically called when execution context ends.
*/
@Override
public void close() throws Exception;
/**
* Action to execute when store is closed.
* @param N the namespace type
*/
@FunctionalInterface
public interface CloseAction<N> {
void close(N namespace, Object key, Object value) throws Exception;
}
}Usage Example:
public class MyExecutionContext implements EngineExecutionContext {
private final NamespacedHierarchicalStore<Namespace> store;
public void setupTestInstance(TestDescriptor testDescriptor) {
// Store test instance in test-specific namespace
Namespace testNamespace = Namespace.create("test", testDescriptor.getUniqueId());
Object testInstance = createTestInstance();
store.put(testNamespace, "instance", testInstance);
// Store database connection with cleanup
DataSource dataSource = store.getOrComputeIfAbsent(
Namespace.create("database"),
"connection",
key -> createDatabaseConnection()
);
// Store temporary files with automatic cleanup
Path tempDir = store.getOrComputeIfAbsent(
testNamespace,
"temp.dir",
key -> {
try {
Path dir = Files.createTempDirectory("test-");
// Register cleanup action
store.put(testNamespace, "temp.dir.cleanup",
(CloseAction<Namespace>) (ns, k, v) -> deleteDirectory((Path) v));
return dir;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);
}
public <T> T getTestScopedValue(TestDescriptor testDescriptor, String key, Class<T> type) {
Namespace testNamespace = Namespace.create("test", testDescriptor.getUniqueId());
return store.get(testNamespace, key, type);
}
public void cleanupTest(TestDescriptor testDescriptor) {
// Create child store for this test - will be automatically closed
try (NamespacedHierarchicalStore<Namespace> testStore = store.newChild()) {
Namespace testNamespace = Namespace.create("test", testDescriptor.getUniqueId());
// Test-specific operations...
// Store will be automatically closed, executing cleanup actions
}
}
}Represents namespaces for organizing data within the hierarchical store.
/**
* Represents a namespace for organizing data in hierarchical stores.
*/
public final class Namespace {
/**
* Create a namespace from parts.
* @param parts the namespace parts
* @return namespace instance
*/
public static Namespace create(Object... parts);
/**
* Get namespace parts.
* @return list of namespace parts
*/
public List<Object> getParts();
// Common namespaces
public static final Namespace GLOBAL = create("global");
}Exception handling for hierarchical store operations.
/**
* Exception thrown by hierarchical store operations.
*/
public class NamespacedHierarchicalStoreException extends RuntimeException {
/**
* Create store exception with message.
* @param message the exception message
*/
public NamespacedHierarchicalStoreException(String message);
/**
* Create store exception with message and cause.
* @param message the exception message
* @param cause the underlying cause
*/
public NamespacedHierarchicalStoreException(String message, Throwable cause);
}Resource Management with Cleanup:
public class DatabaseTestSupport {
private final NamespacedHierarchicalStore<Namespace> store;
public DataSource getDatabaseConnection(TestDescriptor testDescriptor) {
Namespace dbNamespace = Namespace.create("database", testDescriptor.getUniqueId());
return store.getOrComputeIfAbsent(dbNamespace, "connection", key -> {
try {
DataSource dataSource = createDataSource();
// Register cleanup action
store.put(dbNamespace, "connection.cleanup",
(NamespacedHierarchicalStore.CloseAction<Namespace>)
(namespace, k, value) -> {
if (value instanceof DataSource) {
closeDataSource((DataSource) value);
}
});
return dataSource;
} catch (SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
});
}
}Hierarchical Configuration:
public class ConfigurableTestEngine implements TestEngine {
@Override
public void execute(ExecutionRequest request) {
ConfigurationParameters config = request.getConfigurationParameters();
NamespacedHierarchicalStore<Namespace> store = request.getStore();
// Store global configuration
Namespace globalConfig = Namespace.create("config", "global");
store.put(globalConfig, "parallel.enabled",
config.getBoolean("parallel.enabled").orElse(false));
store.put(globalConfig, "timeout.seconds",
config.get("timeout.seconds", Integer::parseInt).orElse(30));
// Execute test hierarchy with inherited configuration
executeTestHierarchy(request.getRootTestDescriptor(), store);
}
private void executeTestHierarchy(TestDescriptor descriptor,
NamespacedHierarchicalStore<Namespace> store) {
// Create child store for this test level
try (NamespacedHierarchicalStore<Namespace> childStore = store.newChild()) {
Namespace testConfig = Namespace.create("config", descriptor.getUniqueId());
// Test-specific configuration inherits from parent through hierarchy
Boolean parallelEnabled = childStore.get(
Namespace.create("config", "global"),
"parallel.enabled",
Boolean.class
);
// Override for specific tests if needed
if (hasCustomParallelSetting(descriptor)) {
childStore.put(testConfig, "parallel.enabled", false);
}
// Execute with child store context
executeWithStore(descriptor, childStore);
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-junit-platform--junit-platform-engine