Skip to main content

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:

application.conf
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โ€‹