JUnit Platform Engine API - Core engine API for implementing custom test engines in the JUnit Platform ecosystem
—
Test execution infrastructure providing contexts, listeners, and result reporting for test engines. This system enables engines to execute discovered tests and communicate results back to the platform.
Provides test engines with everything needed to execute discovered tests, including the test descriptor tree, execution listener, configuration, and test-scoped storage.
/**
* Request object providing context and resources for test execution.
*/
public final class ExecutionRequest {
/**
* Get the root test descriptor to execute.
* @return root test descriptor containing test hierarchy
*/
public TestDescriptor getRootTestDescriptor();
/**
* Get the engine execution listener for reporting test events.
* @return execution listener for this request
*/
public EngineExecutionListener getEngineExecutionListener();
/**
* Get configuration parameters for test execution.
* @return configuration parameters
*/
public ConfigurationParameters getConfigurationParameters();
/**
* Get the output directory provider for file outputs.
* @return output directory provider
*/
public OutputDirectoryProvider getOutputDirectoryProvider();
/**
* Get the request-scoped hierarchical store.
* @return namespaced hierarchical store for request-scoped data
*/
public NamespacedHierarchicalStore<Namespace> getStore();
}Usage Example:
@Override
public void execute(ExecutionRequest request) {
TestDescriptor rootDescriptor = request.getRootTestDescriptor();
EngineExecutionListener listener = request.getEngineExecutionListener();
ConfigurationParameters config = request.getConfigurationParameters();
// Start execution of root descriptor
listener.executionStarted(rootDescriptor);
try {
// Execute all child descriptors
for (TestDescriptor child : rootDescriptor.getChildren()) {
executeDescriptor(child, listener, config);
}
listener.executionFinished(rootDescriptor, TestExecutionResult.successful());
} catch (Exception e) {
listener.executionFinished(rootDescriptor, TestExecutionResult.failed(e));
}
}Listener interface for receiving test execution events and results. Test engines use this to communicate execution progress and outcomes.
/**
* Listener for test execution events, enabling engines to report progress and results.
*/
public interface EngineExecutionListener {
/**
* Called when a dynamic test is registered during execution.
* @param testDescriptor the dynamically registered test descriptor
*/
void dynamicTestRegistered(TestDescriptor testDescriptor);
/**
* Called when execution of a test or container is skipped.
* @param testDescriptor the skipped test descriptor
* @param reason the reason for skipping
*/
void executionSkipped(TestDescriptor testDescriptor, String reason);
/**
* Called when execution of a test or container starts.
* @param testDescriptor the test descriptor being executed
*/
void executionStarted(TestDescriptor testDescriptor);
/**
* Called when execution of a test or container finishes.
* @param testDescriptor the test descriptor that finished
* @param testExecutionResult the execution result
*/
void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult);
/**
* Called when a reporting entry is published during test execution.
* @param testDescriptor the test descriptor publishing the entry
* @param entry the report entry with key-value data
*/
void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry);
/**
* Called when a file entry is published during test execution.
* @param testDescriptor the test descriptor publishing the file
* @param fileEntry the file entry attachment
*/
void fileEntryPublished(TestDescriptor testDescriptor, FileEntry fileEntry);
/**
* No-operation listener implementation.
*/
EngineExecutionListener NOOP = new EngineExecutionListener() {
// All methods have empty implementations
};
}Usage Example:
private void executeTest(TestDescriptor testDescriptor, EngineExecutionListener listener) {
listener.executionStarted(testDescriptor);
try {
// Execute the actual test logic
Object testInstance = createTestInstance(testDescriptor);
Method testMethod = getTestMethod(testDescriptor);
// Publish reporting entry before execution
ReportEntry entry = ReportEntry.from("phase", "execution");
listener.reportingEntryPublished(testDescriptor, entry);
// Execute test method
testMethod.invoke(testInstance);
// Report successful completion
listener.executionFinished(testDescriptor, TestExecutionResult.successful());
} catch (AssertionError e) {
// Test failed
listener.executionFinished(testDescriptor, TestExecutionResult.failed(e));
} catch (Exception e) {
// Test aborted due to unexpected error
listener.executionFinished(testDescriptor, TestExecutionResult.aborted(e));
}
}Classes for publishing structured reporting data and file attachments during test execution.
/**
* Time-stamped key-value pairs for structured reporting during test execution.
*/
public final class ReportEntry {
/**
* Create a report entry from a map of key-value pairs.
* @param keyValuePairs map of keys to values
* @return ReportEntry with current timestamp
*/
public static ReportEntry from(Map<String, String> keyValuePairs);
/**
* Create a report entry from a single key-value pair.
* @param key the report key
* @param value the report value
* @return ReportEntry with current timestamp
*/
public static ReportEntry from(String key, String value);
/**
* Get the key-value pairs for this report entry.
* @return unmodifiable map of key-value pairs
*/
public Map<String, String> getKeyValuePairs();
/**
* Get the timestamp when this entry was created.
* @return creation timestamp
*/
public LocalDateTime getTimestamp();
}
/**
* File attachment for test execution reporting.
*/
public final class FileEntry {
/**
* Create a file entry from a path and media type.
* @param path path to the file
* @param mediaType media type of the file
* @return FileEntry instance
*/
public static FileEntry from(Path path, String mediaType);
/**
* Create a file entry from a path with content type detection.
* @param path path to the file
* @return FileEntry with detected media type
*/
public static FileEntry from(Path path);
/**
* Get the file path.
* @return path to the file
*/
public Path getPath();
/**
* Get the media type.
* @return media type of the file
*/
public String getMediaType();
}Usage Example:
// Publish structured reporting data
Map<String, String> data = Map.of(
"test.type", "integration",
"database.url", "jdbc:h2:mem:test",
"execution.time", "1.234s"
);
ReportEntry entry = ReportEntry.from(data);
listener.reportingEntryPublished(testDescriptor, entry);
// Publish file attachments
Path screenshotPath = captureScreenshot();
FileEntry fileEntry = FileEntry.from(screenshotPath, "image/png");
listener.fileEntryPublished(testDescriptor, fileEntry);Interface for providing output directories where test engines can write files during execution.
/**
* Provider for output directories where test engines can write execution artifacts.
*/
public interface OutputDirectoryProvider {
/**
* Get the default output directory for the current test execution.
* @return path to the default output directory
*/
Path getDefaultOutputDirectory();
/**
* Get a subdirectory within the default output directory.
* @param subdirectoryName name of the subdirectory
* @return path to the subdirectory (created if necessary)
*/
Path getOutputDirectory(String subdirectoryName);
/**
* Get an output directory for a specific test descriptor.
* @param testDescriptor the test descriptor
* @param subdirectoryName optional subdirectory name
* @return path to the output directory for the test
*/
Path getOutputDirectory(TestDescriptor testDescriptor, String subdirectoryName);
}Support for registering tests dynamically during execution, enabling parameterized and data-driven test patterns.
Usage Example:
// In a test engine's execute method
private void executeDynamicContainer(TestDescriptor containerDescriptor,
EngineExecutionListener listener) {
listener.executionStarted(containerDescriptor);
// Generate dynamic tests based on data
List<TestData> testData = loadTestData();
for (TestData data : testData) {
// Create dynamic test descriptor
UniqueId dynamicId = containerDescriptor.getUniqueId()
.append("dynamic", data.getName());
TestDescriptor dynamicTest = createDynamicTestDescriptor(dynamicId, data);
// Register dynamic test
containerDescriptor.addChild(dynamicTest);
listener.dynamicTestRegistered(dynamicTest);
// Execute the dynamic test
executeDynamicTest(dynamicTest, data, listener);
}
listener.executionFinished(containerDescriptor, TestExecutionResult.successful());
}Integration with the configuration system for accessing execution parameters.
Usage Example:
@Override
public void execute(ExecutionRequest request) {
ConfigurationParameters config = request.getConfigurationParameters();
// Get configuration values
boolean parallelExecution = config.getBoolean("junit.jupiter.execution.parallel.enabled")
.orElse(false);
int timeout = config.get("test.timeout", Integer::parseInt)
.orElse(30);
String environment = config.get("test.environment")
.orElse("development");
// Use configuration in execution logic
if (parallelExecution) {
executeInParallel(request);
} else {
executeSequentially(request);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-junit-platform--junit-platform-engine