Skip to main content

Architecture

KTestify is built on a strict three-layer separation of concerns. Each layer has one job and knows nothing about the other layers' implementation details.

Since version 1.1.1, the transport layer has two sibling contracts instead of one. RecordFetcher<V> models a background stream you poll until a record appears (Kafka, Azure Blob). RequestResponseClient<Req, V> models a synchronous, caller-initiated call where you send a request right now and get an answer immediately (HTTP, gRPC, SOAP). Both return List<ConsumedRecord<V>>, so the orchestration and assertion layers below stay identical no matter which contract a given transport implements.

ConsumedRecord<V> is the only data type that crosses layer boundaries.

Synchronous transports additionally populate a new attributes map on ConsumedRecord (an HTTP status code, a future gRPC status code, an MQ reason code) for metadata that is not a protocol header. Asynchronous transports leave it empty. See Core Concepts โ†’ for the full field list.


Layer responsibilitiesโ€‹

Transport - RecordFetcher<V>โ€‹

Knows: Kafka broker, partitions, offsets, deduplication. Does NOT know: matchers, files, test frameworks.

The contract is a single interface:

public interface RecordFetcher<V> extends AutoCloseable {
List<ConsumedRecord<V>> fetch() throws FetchException;
void close();
}

Swapping Kafka for IBM MQ means writing a new IbmMqRecordFetcher<V>, nothing else changes.


Transport, synchronous - RequestResponseClient<Req, V>โ€‹

Knows: how to send one request and turn the answer into ConsumedRecord. Does NOT know: matchers, files, test frameworks, and does not block waiting for something to "appear".

public interface RequestResponseClient<Req, V> extends AutoCloseable {
List<ConsumedRecord<V>> execute(Req request) throws FetchException;
void close();
}

This is the contract an HTTP client plugin implements. See Synchronous Transports โ†’ for the full guide.


Orchestration - AbstractKafkaConsumerโ€‹

Knows: fetch โ†’ match โ†’ result wiring. Does NOT know: Kafka internals, comparison algorithms.

// AbstractKafkaConsumer.call(), simplified
var fetcher = new KafkaRecordFetcher(context);
try {
List<ConsumedRecord<V>> records = fetcher.fetch(); // transport
MatchContext matchCtx = buildMatchContext();
MatchResult result = matcher.match(records, matchCtx); // assertion
return result.isPassed();
} catch (FetchException e) {
throw new ConsumerException(e.getMessage(), e);
} finally {
fetcher.close();
}

Orchestration, synchronous - AbstractSynchronousConsumer<Req, V>โ€‹

Knows: buildRequest โ†’ execute โ†’ match โ†’ result wiring, for transports where the caller supplies the request explicitly instead of the fetcher blocking on a subscription. Does NOT know: HTTP, gRPC, or any transport internals.

// AbstractSynchronousConsumer.call(), simplified
try {
Req request = buildRequest();
List<ConsumedRecord<V>> records = client.execute(request); // transport
MatchResult result = matcher.match(records, buildMatchContext()); // assertion
return result.isPassed();
} catch (FetchException e) {
throw new ConsumerException(e.getMessage());
}

Unlike AbstractKafkaConsumer, the client is not closed in a finally block here. A RequestResponseClient is expected to be a longer lived, connection pooled client (like java.net.http.HttpClient), owned and closed by the plugin's shared scenario resources, not created and discarded per call.


Assertion - RecordMatcher<V>โ€‹

Knows: ConsumedRecord, expected values, comparison algorithm. Does NOT know: Kafka, IBM MQ, any transport.

@FunctionalInterface
public interface RecordMatcher<V> {
MatchResult match(List<ConsumedRecord<V>> records, MatchContext context);
}

Module boundariesโ€‹

ktestify-core ktestify-cucumber
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RecordFetcher<V> BackgroundStepDefinition
RequestResponseClient<Req,V> ValidationStepDefinition
KafkaRecordFetcher โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ConsumerContext (config only)
AbstractKafkaConsumer ConsumerValidationService
AbstractSynchronousConsumer
PollingRequestResponseClient
RecordMatcher<V>
MatchContext / MatchResult
ConsumedRecord<V>

ktestify-cucumber must never import org.apache.kafka.*. It uses ConsumerContext / ProducerContext (ktestify-core abstractions) to configure the engine and receives only ConsumedRecord<V> back.


Class hierarchyโ€‹

Matchers hierarchyโ€‹

RecordMatcher<V> (@FunctionalInterface)
โ”œโ”€โ”€ NoOpRecordMatcher<V>
โ”œโ”€โ”€ FileRecordMatcher
โ”œโ”€โ”€ XmlRecordMatcher
โ”œโ”€โ”€ XPathRecordMatcher
โ”œโ”€โ”€ FieldsRecordMatcher
โ”œโ”€โ”€ FileKeyRecordMatcher
โ”œโ”€โ”€ KeyRecordMatcher
โ”œโ”€โ”€ AttributeRecordMatcher<V>
โ”œโ”€โ”€ AvroFileRecordMatcher
โ”œโ”€โ”€ AvroFileKeyRecordMatcher
โ”œโ”€โ”€ AvroFieldsRecordMatcher
โ””โ”€โ”€ AvroKeyRecordMatcher

See alsoโ€‹