Plugin System
KTestify includes a powerful plugin system that allows you to extend the framework with new transports and capabilities, all discovered and loaded automatically at runtime without hard-coding dependencies. Plugins follow a Service Provider Interface (SPI) pattern using Java's ServiceLoader for seamless integration.
Want to build your own plugin? Jump straight to Creating a Plugin.
How It Worksโ
KTestify's plugin system has two key loading phases:
Phase 1: Classpath Pluginsโ
Plugins bundled as Maven dependencies are discovered via ServiceLoader on startup. This works because the project's build system (Maven Shade) automatically merges all META-INF/services descriptors from bundled JARs into the final artifact.
Phase 2: External Directory Pluginsโ
Third-party plugins can be placed as .jar files in a directory configured by ktestify.plugins.dir (default: /workspace/plugins). Each JAR is loaded independently via a dedicated URLClassLoader and its plugins are discovered automatically, without conflicts with classpath plugins.
This two-phase approach lets you:
- Ship plugins with your test framework as dependencies
- Drop third-party plugins at runtime without recompilation
- Keep plugin JAR versions independent
Lifecycleโ
Every plugin goes through a predictable lifecycle:
- Discovery, The plugin system scans for implementations of
KtestifyPlugin(first classpath, then external JARs) - Initialization,
initialize(PluginContext)is called exactly once before any Cucumber scenario runs - Scenario Execution, Steps from the plugin's glue package are available for use in Gherkin
- Shutdown,
shutdown()is called once on JVM shutdown to release resources
If any plugin's initialize() method throws an exception, the entire test run is aborted with a PluginException.
Core Typesโ
KtestifyPlugin interfaceโ
The contract that every plugin must implement:
package io.github.ktestify.plugin;
public interface KtestifyPlugin {
/**
* Unique, stable plugin identifier (e.g. "azure-blob", "ibm-mq").
* Use kebab-case, as this ID appears in HOCON config paths and logs.
*/
String getId();
/**
* Plugin version string (e.g. "1.0.0").
* Used for informational logging and diagnostics only.
*/
String getVersion();
/**
* Display name of the plugin author or team.
* Shown in the startup banner next to the plugin ID and version.
* Default: "unknown"
*/
default String getAuthorName() { return "unknown"; }
/**
* Contact email of the plugin author or team.
* Displayed alongside the author name in startup logs.
* Default: empty string
*/
default String getAuthorEmail() { return ""; }
/**
* Fully-qualified Java package containing this plugin's Cucumber steps.
* Example: "io.github.ktestify.azureblob.steps"
*
* The ktestify runtime injects this as a --glue argument to Cucumber,
* so step definitions are discovered automatically.
* Return null or empty string if the plugin has no steps.
*/
String getGluePackage();
/**
* Initialize the plugin exactly once at startup.
* Called after config is loaded but before any Cucumber scenario runs.
*
* Use this to:
* - Validate plugin-specific configuration
* - Create shared clients or connections
* - Initialize thread pools or singleton services
*
* Throw PluginException to abort the test run.
*/
void initialize(PluginContext context);
/**
* Shut down the plugin exactly once on JVM exit.
* Called after all Cucumber scenarios have finished.
*
* Use this to release resources (connections, threads, etc.).
* Do NOT throw exceptions, log and swallow instead.
*/
void shutdown();
}
PluginContext interfaceโ
Passed to initialize(), provides controlled access to framework configuration:
package io.github.ktestify.plugin;
public interface PluginContext {
/**
* Returns the fully loaded KtestifyConfig singleton.
*
* Call getRaw() on the result to navigate to your plugin's
* ktestify.plugins.<id> subtree, or access shared settings
* like ktestify.framework.directories.assets.
*/
KtestifyConfig getConfig();
}
Example - reading plugin config:
@Override
public void initialize(PluginContext context) {
Config fullConfig = context.getConfig().getRaw();
// Access your plugin's subtree
if (fullConfig.hasPath("ktestify.plugins.my-plugin")) {
Config pluginCfg = fullConfig.getConfig("ktestify.plugins.my-plugin");
String endpoint = pluginCfg.getString("endpoint");
Duration timeout = pluginCfg.getDuration("timeout");
}
}
PluginRegistry classโ
The central registry that discovers, loads, and manages all plugins:
package io.github.ktestify.plugin;
public final class PluginRegistry {
/**
* Discovers and initializes all plugins (classpath + external).
* Call this once at startup before Cucumber runs.
*/
public static PluginRegistry load(PluginContext ctx);
/**
* Get all loaded plugins.
*/
public List<KtestifyPlugin> getPlugins();
/**
* Get the Cucumber glue packages from all plugins.
* Add each as --glue <package> when running Cucumber.
*/
public List<String> getGluePackages();
/**
* Shut down all plugins in reverse initialization order.
*/
public void shutdown();
}
Plugin Configurationโ
Plugins read their configuration from the HOCON file under ktestify.plugins.<id>. This convention keeps plugin settings isolated and organized:
ktestify {
plugins {
# Azure Blob Storage plugin
azure-blob {
connection-string = ${?AZURE_STORAGE_CONNECTION_STRING}
timeout = 30s
auto-create-containers = true
}
# IBM MQ plugin (hypothetical)
ibm-mq {
host = "localhost"
port = 1414
channel = "DEV.ADMIN.SVRCONN"
queue-manager = "QM1"
}
}
}
Configuration resolution order:
- System properties (e.g.
-Dktestify.plugins.my-plugin.timeout=60s) - Environment variables (recommended for secrets, e.g.
KTESTIFY_PLUGINS_MY_PLUGIN_ENDPOINT) application.confin classpath- Plugin's
reference.conf(defaults)
Available pluginsโ
| Plugin | Artifact | Status | Docs |
|---|---|---|---|
| Azure Blob Storage | ktestify-plugin-azureblob | ๐ง In progress | Azure Blob โ |
| S3 | , | ๐ก Planned | , |
| IBM MQ | , | ๐ก Planned | , |
| SFTP | , | ๐ก Planned | , |
Example Plugin Architectureโ
Here is how a typical plugin is structured:
ktestify-plugin-example/
โโโ src/main/java/io/github/ktestify/example/
โ โโโ ExamplePlugin.java โ implements KtestifyPlugin
โ โโโ config/
โ โ โโโ ExampleConfig.java โ reads plugin HOCON config
โ โโโ transport/
โ โ โโโ ExampleRecordFetcher.java โ implements RecordFetcher<String>
โ โ โโโ ExampleClient.java โ wraps external SDK
โ โโโ orchestration/
โ โ โโโ ExampleConsumer.java โ orchestrates fetch โ match
โ โโโ steps/
โ โโโ BackgroundSteps.java โ @Given steps
โ โโโ ActionSteps.java โ @When steps
โ โโโ ValidationSteps.java โ @Then steps
โโโ src/main/resources/
โ โโโ META-INF/services/
โ โ โโโ io.github.ktestify.plugin.KtestifyPlugin
โ โโโ reference.conf โ default config
โ โโโ log4j2.properties
โโโ src/test/java/io/github/ktestify/example/
โ โโโ ...test files...
โโโ pom.xml
โโโ README.md
Most plugins follow the three-layer separation defined by ktestify-core:
- Transport,
ExampleRecordFetcherimplementsRecordFetcher<String>, fetches records from the external system - Orchestration,
ExampleConsumerwires fetch โ match โ result - Assertion, uses standard
RecordMatcherimplementations from ktestify-core
Startup Log Exampleโ
When ktestify starts with plugins, you'll see a banner like this:
โโโ KTestify Plugin System โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ [classpath] Discovered plugin: azure-blob v1.0, author: John Doe <john@example.com>
โ Plugin 'azure-blob' initialized successfully.
โ [external] Scanning '/workspace/plugins', 2 JAR(s) found.
โ [external] Discovered plugin: s3 v2.1, author: Jane Smith <jane@example.com>
โ Plugin 's3' initialized successfully.
โ 3 plugin(s) active: [azure-blob@1.0, s3@2.1, ibm-mq@1.5]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Best Practicesโ
- Use kebab-case plugin IDs, e.g.
azure-blob, notazureBloborAZURE_BLOB - Namespace your config, always use
ktestify.plugins.<id>to avoid conflicts - Validate in
initialize(), fail fast withPluginExceptionif critical config is missing - Release resources in
shutdown(), close connections, stop threads, etc. - Log appropriately, use
@Slf4j/ SLF4J for diagnostics - Document config keys, provide a
reference.confwith all config keys and defaults - Register via SPI, declare your plugin class in
META-INF/services/io.github.ktestify.plugin.KtestifyPlugin - Keep dependencies minimal, avoid bloating the classpath with transitive deps