Adding a New Transport
New transports in KTestify are plugins, self-contained Maven modules that hook into the framework at runtime without touching any existing code. This is the same model used by ktestify-plugin-azureblob.
Before reading this page, make sure you understand the Plugin System โ.
Why the plugin modelโ
KTestify's transport layer is fully abstracted behind RecordFetcher<V> and ConsumedRecord<V>. All existing matchers (FileRecordMatcher, XmlRecordMatcher, AvroFileRecordMatcher, โฆ) work on List<ConsumedRecord<V>> regardless of where those records came from. A new transport only needs to produce ConsumedRecord objects, everything else is already done.
The plugin system lets you ship that new transport as a separate JAR dropped into /workspace/plugins at runtime, no rebuild of ktestify-core or ktestify-cucumber required.
Step 1 - Create a Maven moduleโ
ktestify-plugin-ibmmq/
โโโ pom.xml
โโโ src/main/java/io/github/ktestify/plugin/ibmmq/
โโโ IbmMqPlugin.java โ implements KtestifyPlugin
โโโ IbmMqRecordFetcher.java โ implements RecordFetcher<V>
โโโ IbmMqConsumerContext.java
โโโ steps/
โโโ IbmMqStepDefinitions.java โ Cucumber step definitions
Your pom.xml declares ktestify-core as its only KTestify dependency:
<dependency>
<groupId>io.github.ktestify</groupId>
<artifactId>ktestify-core</artifactId>
<version>${ktestify.version}</version>
</dependency>
Step 2 - Implement RecordFetcher<V>โ
This is the transport contract. Wrap each incoming message as a ConsumedRecord<V> and throw FetchException on timeout or broker error:
public class IbmMqRecordFetcher<V> implements RecordFetcher<V> {
private final IbmMqConsumerContext context;
@Override
public List<ConsumedRecord<V>> fetch() throws FetchException {
try {
Message msg = consumer.receive(context.getReadTimeoutMs());
if (msg == null) {
throw new FetchException("Timed out waiting for a message on queue '"
+ context.getQueueName() + "'");
}
return List.of(ConsumedRecord.<V>builder()
.source(context.getQueueName())
.partition(0)
.offset(0)
.key(msg.getJMSCorrelationID())
.value((V) msg.getBody(String.class))
.timestamp(msg.getJMSTimestamp())
.build());
} catch (JMSException e) {
throw new FetchException("IBM MQ error: " + e.getMessage(), e);
}
}
@Override
public void close() { /* close JMS session/connection */ }
}
Step 3 - Implement KtestifyPluginโ
Register your fetcher and any Cucumber step definitions through the plugin interface:
public class IbmMqPlugin implements KtestifyPlugin {
@Override
public String getPluginId() { return "ibmmq"; }
@Override
public void initialize(PluginContext context) {
// read connection details from HOCON config
String host = context.getConfig().getConfig().getString("ktestify.ibmmq.host");
int port = context.getConfig().getConfig().getInt("ktestify.ibmmq.port");
String manager = context.getConfig().getConfig().getString("ktestify.ibmmq.queue-manager");
// set up JMS connection factory, store on the plugin instance
}
}
Register it in a Cucumber @Before hook inside your step definitions class:
@Before(order = 0)
public void registerPlugin() {
PluginRegistry.register(new IbmMqPlugin());
}
Step 4 - Add Gherkin stepsโ
Create step definitions that retrieve your plugin from PluginRegistry and expose the Gherkin DSL:
@Given("output queue")
public void givenOutputQueue(DataTable table) {
// register queue in shared resources
}
@Then("expected queue record from file")
public void thenExpectedQueueRecord(DataTable table) {
IbmMqPlugin plugin = (IbmMqPlugin) PluginRegistry.get("ibmmq");
// build IbmMqConsumerContext from table
// run IbmMqRecordFetcher โ matcher โ assert
}
Resulting Gherkin:
Given output queue
| queueName | queueAlias |
| MY.OUT.QUEUE | orders-out |
Then expected queue record from file
| queueAlias | file |
| orders-out | expected.txt |
Step 5 - Plugin configurationโ
By convention, read your settings from under the ktestify namespace:
ktestify {
ibmmq {
host = "mq.internal"
port = 1414
queue-manager = "QM1"
channel = "DEV.APP.SVRCONN"
}
}
Step 6 - Drop the JAR into /workspace/pluginsโ
Build your plugin JAR and mount it into the container. KTestify scans /workspace/plugins at startup and loads any JARs it finds:
docker run --rm \
-v "$(pwd)/workspace/features:/workspace/features" \
-v "$(pwd)/plugins:/workspace/plugins" \
-e KTESTIFY_BOOTSTRAP_SERVERS=kafka:9092 \
ghcr.io/ktestify/ktestify-cucumber:latest \
/workspace/features
No rebuild of ktestify-cucumber required. The plugin is discovered and its step definitions are picked up automatically by Cucumber's classpath scanning.
See alsoโ
- Plugin System โ,
KtestifyPlugin,PluginRegistry,PluginContext - Azure Blob plugin โ, a real example of the plugin pattern
- Architecture โ, how
RecordFetcher<V>andConsumedRecord<V>keep transports isolated