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
- Synchronous transports →, the
RequestResponseClient<Req, V>contract for HTTP, gRPC, and other call/response transports - Architecture →, how
RecordFetcher<V>andConsumedRecord<V>keep transports isolated