ktestify-cucumber is the standalone BDD test runner of the ktestify ecosystem. It wraps ktestify-core a Cucumber layer & Gherkin step definitions, PicoContainer dependency injection, and a ready-to-run Docker image, so you can write Kafka integration tests in plain English without a single line of Java.
docker run --rm \
-v $(pwd)/features:/workspace/features \
-v $(pwd)/assets:/workspace/assets \
-v $(pwd)/config:/workspace/config \
-v $(pwd)/reports:/workspace/reports \
-e KAFKA_BOOTSTRAP_SERVERS=kafka:9092 \
-e SCHEMA_REGISTRY_URL=http://schema-registry:8081 \
-e config.file=/workspace/config/application.conf \
ghcr.io/ktestify/ktestify-cucumber:latest \
/workspace/featuresjava -Dconfig.file=src/test/resources/local.conf \
-jar target/ktestify-cucumber.jar \
src/test/resources/featuresTests are written in standard Gherkin inside .feature files. A complete roundtrip example:
Feature: Order processing — raw roundtrip
Background:
Given namespace
| namespace |
| my-org |
Given input topic
| topicName | topicAlias |
| orders | orders-in |
Given output topic
| topicName | topicAlias |
| orders-processed | orders-out |
Given assets directory
| absolutePath |
| src/test/resources/data/orders |
Scenario: Processed order matches expected file
When record from file is sent
| topicName | file | recordKey |
| orders | order.json | ORD-001 |
Then expected record from file
| topicAlias | file | expectedRecordKey | consumerReadTimeout |
| orders-out | expected-order.json | ORD-001 | 30 || Step | Purpose |
|---|---|
Given namespace |
Register a single topic namespace |
Given namespaces |
Register multiple namespaces with aliases |
Given input topic |
Register a producer topic |
Given output topic |
Register a consumer topic |
Given assets directory |
Set the base path for payload/expected files |
Given schema |
Register an Avro schema (name + alias + version) |
Given input queue |
Register an IBM MQ input queue |
Given output queue |
Register an IBM MQ output queue |
Given CFT hosts |
Register a CFT file-transfer host |
| Step | Purpose |
|---|---|
When record from file is sent |
Produce a raw (String) record |
When record from file based on schema is sent |
Produce an Avro record |
When queue record from file is sent |
Send a message to an IBM MQ queue |
When file is sent using CFT |
Transfer a file via CFT |
And wait for {int} seconds |
Sleep between steps |
When script is executed |
Run a shell script |
| Step | Purpose |
|---|---|
Then expected record from file |
Raw file match |
Then expected record should have fields matching from file |
Positional field match (line/from/to) |
Then expected record from file based on XML |
XML structural match with optional exclusions |
Then expected record based on XML should have fields matching from file |
XPath expression match |
Then expected records from files |
Batch raw records vs. multiple files |
Then expected record from file based on schema |
Avro file match with optional excluded keys |
Then expected record based on schema should have fields matching from given value |
Avro field/value match |
Then expected records from files based on schema |
Batch Avro records |
And record should not appear in topic |
Negative assertion (no record within timeout) |
And record should appear in topic |
Positive presence assertion |
All validation steps that accept an expectedRecordKey column also accept an optional keyMatchStrategy column. See Key matching strategies below.
When a step includes an expectedRecordKey column, you can optionally add a keyMatchStrategy column to control how the key is matched. This is useful when record keys are dynamically generated (UUIDs, prefixed keys) and exact match is too restrictive.
| Strategy | Value | Behaviour |
|---|---|---|
EXACT |
exact |
Exact string equality (default) |
CONTAINS |
contains |
Expected key is a substring of actual |
STARTS_WITH |
starts_with |
Actual key starts with expected |
ENDS_WITH |
ends_with |
Actual key ends with expected |
REGEX |
regex |
Actual key matches Java regex pattern |
# Match a UUID-prefixed key without knowing the full UUID
Then expected record from file
| topicAlias | file | expectedRecordKey | keyMatchStrategy | consumerReadTimeout |
| orders-out | expected.json | 550e8400 | starts_with | 30 |The value is case-insensitive and tolerant of hyphens/underscores (starts-with, starts_with, StartsWith all work). Unrecognised values default to exact.
When using batch validation steps (Then expected records from files or Then expected records from files based on schema), you can optionally include expectedRecordKeys (comma-separated keys paired 1-to-1 with files) and a batchMatchStrategy column to control how consumed records are paired with expected files and keys.
| Strategy | Accepted values | Behaviour |
|---|---|---|
POSITIONAL |
positional, ordered, index, strict |
Record at index i must match file at index i (and expectedRecordKeys[i] when set) (default) |
LAZY |
lazy, fuzzy, unordered, any_order, any-order |
Order-independent 1-to-1 matching without replacement (matches both key and file when expectedRecordKeys is set) |
# Positional (default): record 1 -> file 1, record 2 -> file 2, record 3 -> file 3
Then expected records from files
| topicAlias | expectedRecordsCount | files | expectedRecordKeys | consumerReadTimeout | consumerDeltaTime |
| batch-out | 3 | integration/batch-expected-1.json,integration/batch-expected-2.json,integration/batch-expected-3.json | batch-003,batch-004,batch-005 | 30 | 60 |
# Lazy / Fuzzy: records sent in order (3, 1, 2) still match files listed as (1, 2, 3)
Then expected records from files
| topicAlias | expectedRecordsCount | files | expectedRecordKeys | batchMatchStrategy | consumerReadTimeout | consumerDeltaTime |
| batch-out | 3 | integration/batch-expected-1.json,integration/batch-expected-2.json,integration/batch-expected-3.json | batch-007,batch-008,batch-006 | lazy | 30 | 60 |ktestify-cucumber uses Typesafe Config (HOCON). Override defaults via -Dconfig.file, environment variables, or an application.conf on the classpath.
ktestify {
kafka {
bootstrap-servers = "localhost:9092"
bootstrap-servers = ${?KAFKA_BOOTSTRAP_SERVERS}
topic-namespace = ${?KTESTIFY_TOPIC_NAMESPACE}
consumer.group-id = "my-test-group"
}
schema-registry {
url = "http://localhost:8081"
url = ${?SCHEMA_REGISTRY_URL}
}
framework {
timeouts {
default-read-timeout = 30s # cucumber default (longer than core's 10s)
consumer-delta-time = 60s
}
directories {
assets = "" # set via 'Given assets directory' or KTESTIFY_ASSETS_DIR
}
}
}The image is published to GitHub Container Registry on every release.
ghcr.io/ktestify/ktestify-cucumber:<version>
ghcr.io/ktestify/ktestify-cucumber:latest
| Mount | Default path inside container | Purpose |
|---|---|---|
| Feature files | /workspace/features |
Gherkin .feature files |
| Assets | /workspace/assets |
Payload and expected files |
| Config | /workspace/config |
application.conf (set -Dconfig.file) |
| Reports | /workspace/reports |
Cucumber HTML / JSON reports |
| Plugins | /workspace/plugins |
Drop-in plugin JARs |
The image supports linux/amd64 and linux/arm64 (Apple Silicon / cloud ARM).
Payload and expected files are processed transparently by DynamicVariableProcessor. Use {{VARIABLE}} syntax inside any file read by FileUtils.getFileContent().
| Syntax | Output example |
|---|---|
{{date}} |
2026-05-31 |
{{date:yyyy/MM/dd}} |
2026/05/31 |
{{timestamp}} |
2026-05-31T14:22:00 |
{{random}} |
UUID v4 |
{{random:str:12}} |
aB3xZ9qWmK1p |
{{env:MY_VAR}} |
value of $MY_VAR |
- ktestify-core — the transport and assertion engine
- docs.ktestify.xyz — full documentation and configuration reference
Contributions are welcome. Please read the contributing guide before opening a pull request.
- Fork the repository
- Create a feature branch —
git checkout -b feat/my-feature - Commit with Conventional Commits —
git commit -m "feat: add my feature" - Push and open a Pull Request against
main
ktestify-cucumber is licensed under the Apache License 2.0.
