Skip to main content

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.

tip

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:

  1. Discovery, The plugin system scans for implementations of KtestifyPlugin (first classpath, then external JARs)
  2. Initialization, initialize(PluginContext) is called exactly once before any Cucumber scenario runs
  3. Scenario Execution, Steps from the plugin's glue package are available for use in Gherkin
  4. 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:

  1. System properties (e.g. -Dktestify.plugins.my-plugin.timeout=60s)
  2. Environment variables (recommended for secrets, e.g. KTESTIFY_PLUGINS_MY_PLUGIN_ENDPOINT)
  3. application.conf in classpath
  4. Plugin's reference.conf (defaults)

Available pluginsโ€‹

PluginArtifactStatusDocs
Azure Blob Storagektestify-plugin-azureblob๐Ÿšง In progressAzure 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, ExampleRecordFetcher implements RecordFetcher<String>, fetches records from the external system
  • Orchestration, ExampleConsumer wires fetch โ†’ match โ†’ result
  • Assertion, uses standard RecordMatcher implementations 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โ€‹

  1. Use kebab-case plugin IDs, e.g. azure-blob, not azureBlob or AZURE_BLOB
  2. Namespace your config, always use ktestify.plugins.<id> to avoid conflicts
  3. Validate in initialize(), fail fast with PluginException if critical config is missing
  4. Release resources in shutdown(), close connections, stop threads, etc.
  5. Log appropriately, use @Slf4j / SLF4J for diagnostics
  6. Document config keys, provide a reference.conf with all config keys and defaults
  7. Register via SPI, declare your plugin class in META-INF/services/io.github.ktestify.plugin.KtestifyPlugin
  8. Keep dependencies minimal, avoid bloating the classpath with transitive deps