From 8ac5a355ea94ad6a980b4175200c31fcfe812938 Mon Sep 17 00:00:00 2001 From: Nil MALHOMME Date: Thu, 1 Oct 2026 19:04:26 +0200 Subject: [PATCH 1/3] feat: introduce key matching strategies for record assertions --- .../io/kafka/AbstractKafkaConsumer.java | 29 +-- .../ktestify/io/kafka/ConsumerContext.java | 26 +- .../ktestify/io/kafka/KafkaRecordFetcher.java | 21 +- .../ktestify/match/KeyMatchStrategy.java | 140 +++++++++++ .../github/ktestify/match/MatchContext.java | 17 +- .../match/impl/AvroFileKeyRecordMatcher.java | 7 +- .../match/impl/AvroKeyRecordMatcher.java | 21 +- .../match/impl/FileKeyRecordMatcher.java | 9 +- .../ktestify/match/impl/KeyRecordMatcher.java | 21 +- .../io/kafka/RawKafkaConsumerTest.java | 61 +++-- .../ktestify/match/KeyMatchStrategyTest.java | 238 ++++++++++++++++++ .../match/impl/KeyRecordMatcherTest.java | 90 ++++++- .../match/impl/MatcherTestSupport.java | 12 +- 13 files changed, 594 insertions(+), 98 deletions(-) create mode 100644 src/main/java/io/github/ktestify/match/KeyMatchStrategy.java create mode 100644 src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java diff --git a/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java b/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java index 9acb169..066dd8c 100644 --- a/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java +++ b/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java @@ -22,11 +22,9 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; -import io.github.ktestify.models.Topic; -import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.Consumer; + +import java.util.List; /** * Thin coordinator that wires a {@link KafkaRecordFetcher} (transport) with a {@link RecordMatcher} (assertion) and @@ -66,28 +64,6 @@ protected AbstractKafkaConsumer(ConsumerContext context, RecordMatcher matcher.getClass().getSimpleName()); } - /** - * Legacy convenience constructor for callers that previously passed topic + consumer + properties. - * - * @param topic the topic to consume from - * @param consumer the Kafka consumer instance - * @param properties the consumer properties map - * @param matcher the assertion strategy - * @deprecated Build a {@link ConsumerContext} and use {@link #AbstractKafkaConsumer(ConsumerContext, - * RecordMatcher)} instead. - */ - @Deprecated - protected AbstractKafkaConsumer( - Topic topic, Consumer consumer, Map properties, RecordMatcher matcher) { - this( - ConsumerContext.builder() - .topic(topic) - .consumer(consumer) - .properties(properties) - .build(), - matcher); - } - /** * Fetches records from Kafka, then asserts them with the configured matcher. * @@ -137,6 +113,7 @@ protected MatchContext buildMatchContext() { .matchFilePaths(context.getMatchFilePaths()) .excludedFields(context.getExcludedFields()) .strictMatching(false) + .keyMatchStrategy(context.getKeyMatchStrategy()) .build(); } } diff --git a/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java b/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java index 0dec8a0..1f7a643 100644 --- a/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java +++ b/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java @@ -17,12 +17,14 @@ import io.github.ktestify.config.KtestifyConfig; import io.github.ktestify.exceptions.ConsumerException; +import io.github.ktestify.match.KeyMatchStrategy; import io.github.ktestify.models.Topic; +import lombok.Getter; +import org.apache.kafka.clients.consumer.Consumer; + import java.util.Collections; import java.util.List; import java.util.Map; -import lombok.Getter; -import org.apache.kafka.clients.consumer.Consumer; @Getter public final class ConsumerContext { @@ -31,6 +33,7 @@ public final class ConsumerContext { private final Map properties; private final Consumer consumer; private final String expectedRecordKey; + private final KeyMatchStrategy keyMatchStrategy; private final String matchMethod; private final List matchFilePaths; private final List excludedFields; @@ -45,6 +48,7 @@ private ConsumerContext( Map properties, Consumer consumer, String expectedRecordKey, + KeyMatchStrategy keyMatchStrategy, String matchMethod, List matchFilePaths, List excludedFields, @@ -57,6 +61,7 @@ private ConsumerContext( this.properties = properties; this.consumer = consumer; this.expectedRecordKey = expectedRecordKey; + this.keyMatchStrategy = keyMatchStrategy != null ? keyMatchStrategy : KeyMatchStrategy.EXACT; this.matchMethod = matchMethod; this.matchFilePaths = matchFilePaths != null ? matchFilePaths : Collections.emptyList(); this.excludedFields = excludedFields != null ? excludedFields : Collections.emptyList(); @@ -85,6 +90,7 @@ public static final class Builder { private Map properties; private Consumer consumer; private String expectedRecordKey; + private KeyMatchStrategy keyMatchStrategy; private String matchMethod; private List matchFilePaths; private List excludedFields; @@ -114,6 +120,21 @@ public Builder expectedRecordKey(String expectedRecordKey) { return this; } + /** + * Sets the strategy used to compare {@link #expectedRecordKey} against the actual record key during the + * fetch-time pre-filter in {@code KafkaRecordFetcher.passesKeyFilter()}. + * + *

Defaults to {@link KeyMatchStrategy#EXACT} when not set, preserving backward compatibility. + * + * @param keyMatchStrategy the match strategy, or {@code null} to use the default + * @return this builder + * @since 1.1.5 + */ + public Builder keyMatchStrategy(KeyMatchStrategy keyMatchStrategy) { + this.keyMatchStrategy = keyMatchStrategy; + return this; + } + public Builder matchMethod(String matchMethod) { this.matchMethod = matchMethod; return this; @@ -196,6 +217,7 @@ public ConsumerContext build() { validatedProps, validatedConsumer, expectedRecordKey, + keyMatchStrategy, matchMethod, matchFilePaths, excludedFields, diff --git a/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java b/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java index 045209e..b76433f 100644 --- a/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java +++ b/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java @@ -15,22 +15,12 @@ */ package io.github.ktestify.io.kafka; -import static io.github.ktestify.constants.LogMessagesConstants.*; - import io.github.ktestify.config.FrameworkConfig; import io.github.ktestify.config.KtestifyConfig; import io.github.ktestify.exceptions.FetchException; import io.github.ktestify.io.core.RecordFetcher; import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.models.MatchedRecord; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -39,6 +29,13 @@ import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.WakeupException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static io.github.ktestify.constants.LogMessagesConstants.*; + /** * Kafka implementation of {@link RecordFetcher}. * @@ -337,7 +334,7 @@ private void registerAsMatched(ConsumedRecord record) { /** * Returns {@code true} if no key-filter is configured, or if the record key matches the expected key from the - * context / properties. + * context / properties using the configured {@link io.github.ktestify.match.KeyMatchStrategy}. */ private boolean passesKeyFilter(ConsumerRecord record) { // Context takes priority over properties map @@ -351,7 +348,7 @@ private boolean passesKeyFilter(ConsumerRecord record) { } String recordKey = record.key() != null ? record.key().toString() : null; - if (expectedKey.equals(recordKey)) { + if (context.getKeyMatchStrategy().matches(expectedKey, recordKey)) { log.info(MESSAGE_CONSUMER_RECORD_MATCHES_EXPECTED_KEY, expectedKey); return true; } diff --git a/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java b/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java new file mode 100644 index 0000000..9de1c70 --- /dev/null +++ b/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java @@ -0,0 +1,140 @@ +/* + * Copyright 2026 Nil MALHOMME (malhomme.nil+oss@icloud.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ktestify.match; + +/** + * Strategies for comparing a record key against an expected key. + * + *

Used in two places: + * + *

    + *
  • {@code KafkaRecordFetcher.passesKeyFilter()} as a pre-filter during the Kafka poll loop + *
  • {@code KeyRecordMatcher}, {@code FileKeyRecordMatcher}, {@code AvroKeyRecordMatcher}, and + * {@code AvroFileKeyRecordMatcher} as the post-fetch assertion + *
+ * + *

The default strategy is {@link #EXACT}, which preserves the original {@code String.equals()} behavior. Other + * strategies allow matching dynamically generated keys by prefix, suffix, substring, or regular expression. + * + * @since 1.1.1 + */ +public enum KeyMatchStrategy { + + /** + * Exact equality: {@code expected.equals(actual)}. + * + *

This is the default and preserves backward compatibility for feature files that do not specify a + * {@code keyMatchStrategy} column. + * + * @since 1.1.1 + */ + EXACT { + @Override + public boolean matches(String expected, String actual) { + return expected != null && expected.equals(actual); + } + }, + + /** + * Substring match: {@code actual.contains(expected)}. + * + *

Useful when the record key contains a known fragment embedded in a larger dynamically generated value. + * + * @since 1.1.1 + */ + CONTAINS { + @Override + public boolean matches(String expected, String actual) { + return expected != null && actual != null && actual.contains(expected); + } + }, + + /** + * Prefix match: {@code actual.startsWith(expected)}. + * + *

Useful when the record key starts with a known prefix followed by a dynamically generated suffix (e.g. + * {@code ORD-}). + * + * @since 1.1.1 + */ + STARTS_WITH { + @Override + public boolean matches(String expected, String actual) { + return expected != null && actual != null && actual.startsWith(expected); + } + }, + + /** + * Suffix match: {@code actual.endsWith(expected)}. + * + *

Useful when the record key ends with a known suffix preceded by a dynamically generated prefix. + * + * @since 1.1.1 + */ + ENDS_WITH { + @Override + public boolean matches(String expected, String actual) { + return expected != null && actual != null && actual.endsWith(expected); + } + }, + + /** + * Regular expression match: {@code actual.matches(expected)}. + * + *

The {@code expected} string is interpreted as a Java regular expression. Useful for arbitrary patterns such as + * {@code ORD-\d{6}} that cannot be expressed with prefix, suffix, or substring matching. + * + * @since 1.1.1 + */ + REGEX { + @Override + public boolean matches(String expected, String actual) { + return expected != null && actual != null && actual.matches(expected); + } + }; + + /** + * Tests whether the {@code actual} record key satisfies this strategy given the {@code expected} key. + * + * @param expected the expected key value (or pattern for {@link #REGEX}) + * @param actual the actual record key, may be {@code null} when the Kafka record has no key + * @return {@code true} if the actual key matches according to this strategy + * @since 1.1.1 + */ + public abstract boolean matches(String expected, String actual); + + /** + * Parses a strategy name from a DataTable column value. + * + *

Matching is case-insensitive and tolerant of hyphens, underscores, and spaces. For example, + * {@code "starts_with"}, {@code "starts-with"}, and {@code "STARTS WITH"} all resolve to {@link #STARTS_WITH}. + * + * @param value the raw column value, may be {@code null} or blank + * @return the parsed strategy, or {@link #EXACT} when the value is {@code null}, blank, or unrecognized + * @since 1.1.1 + */ + public static KeyMatchStrategy fromString(String value) { + if (value == null || value.isBlank()) { + return EXACT; + } + String normalized = value.trim().toUpperCase().replace('-', '_').replace(' ', '_'); + try { + return KeyMatchStrategy.valueOf(normalized); + } catch (IllegalArgumentException e) { + return EXACT; + } + } +} diff --git a/src/main/java/io/github/ktestify/match/MatchContext.java b/src/main/java/io/github/ktestify/match/MatchContext.java index 70710aa..5317ec4 100644 --- a/src/main/java/io/github/ktestify/match/MatchContext.java +++ b/src/main/java/io/github/ktestify/match/MatchContext.java @@ -15,11 +15,12 @@ */ package io.github.ktestify.match; +import lombok.Builder; +import lombok.Value; + import java.util.Collections; import java.util.List; import java.util.Map; -import lombok.Builder; -import lombok.Value; /** * Immutable value object that carries all configuration needed by a {@code RecordMatcher}. @@ -73,6 +74,18 @@ public class MatchContext { /** Expected value for {@link #matchKey}. */ String matchValue; + /** + * Strategy used to compare {@link #matchKey} against the actual record key in key-related matchers + * ({@code KeyRecordMatcher}, {@code FileKeyRecordMatcher}, {@code AvroKeyRecordMatcher}, + * {@code AvroFileKeyRecordMatcher}). + * + *

Defaults to {@link KeyMatchStrategy#EXACT}, preserving the original exact-equality behavior. + * + * @since 1.1.5 + */ + @Builder.Default + KeyMatchStrategy keyMatchStrategy = KeyMatchStrategy.EXACT; + /** * Multiple key/value pairs for multi-field inline matching. * diff --git a/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java index 643cd59..0ac9fb0 100644 --- a/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java @@ -23,10 +23,11 @@ import io.github.ktestify.utils.FileUtils; import io.github.ktestify.utils.serdes.AvroDeserializer; import io.github.ktestify.utils.serdes.AvroUtils; -import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.avro.generic.GenericRecord; +import java.util.List; + /** * Asserts both the record key and Avro value against a configured expected key and expected JSON * file. @@ -54,13 +55,13 @@ public MatchResult match(List> records, MatchConte throw new ComparisonException("AvroFileKeyRecordMatcher requires matchFilePath to be set."); } - ConsumedRecord record = records.get(0); + ConsumedRecord record = records.getFirst(); String actualKey = record.getKey(); String expectedKey = context.getMatchKey(); String expectedValue = FileUtils.getFileContent(FileUtils.getFile(context.getMatchFilePath())); String actualValue = toJson(record.getValue()); - boolean keyMatches = expectedKey.equals(actualKey); + boolean keyMatches = context.getKeyMatchStrategy().matches(expectedKey, actualKey); boolean valueMatches = AvroUtils.doesAvroRecordsSmartMatches(AvroUtils.getPrettyAvroValue(expectedValue), actualValue); diff --git a/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java index 152bd0c..e829be1 100644 --- a/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java @@ -20,10 +20,11 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; -import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.avro.generic.GenericRecord; +import java.util.List; + /** * Asserts that the Avro record key equals the expected key in {@link MatchContext#getMatchKey()}. * @@ -41,16 +42,24 @@ public MatchResult match(List> records, MatchConte } String expectedKey = context.getMatchKey(); - String actualKey = records.get(0).getKey(); + String actualKey = records.getFirst().getKey(); - if (expectedKey.equals(actualKey)) { - log.info("Avro record key matches expected key '{}'.", expectedKey); + if (context.getKeyMatchStrategy().matches(expectedKey, actualKey)) { + log.info( + "Avro record key matches expected key '{}' using {} strategy.", + expectedKey, + context.getKeyMatchStrategy()); return MatchResult.pass(expectedKey, actualKey); } - log.error("Avro record key mismatch — expected: '{}', actual: '{}'", expectedKey, actualKey); + log.error( + "Avro record key mismatch, expected: '{}', actual: '{}', strategy: {}", + expectedKey, + actualKey, + context.getKeyMatchStrategy()); return MatchResult.fail( - "Avro record key does not match — expected: '" + expectedKey + "', actual: '" + actualKey + "'.", + "Avro record key does not match, expected: '" + expectedKey + "', actual: '" + actualKey + + "', strategy: " + context.getKeyMatchStrategy() + ".", expectedKey, actualKey); } diff --git a/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java index c7c3033..c754f37 100644 --- a/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java @@ -22,9 +22,10 @@ import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.utils.FileUtils; import io.github.ktestify.utils.StringDiffUtils; -import java.util.List; import lombok.extern.slf4j.Slf4j; +import java.util.List; + /** * Compares both the record key and value against a configured expected key and expected file content. * @@ -50,17 +51,17 @@ public MatchResult match(List> records, MatchContext cont throw new ComparisonException("FileKeyRecordMatcher requires matchFilePath to be set."); } - ConsumedRecord record = records.get(0); + ConsumedRecord record = records.getFirst(); String expectedValue = FileUtils.getFileContent(FileUtils.getFile(context.getMatchFilePath())); String actualValue = record.getValue(); String expectedKey = context.getMatchKey(); String actualKey = record.getKey(); - boolean keyMatches = expectedKey.equals(actualKey); + boolean keyMatches = context.getKeyMatchStrategy().matches(expectedKey, actualKey); boolean valueMatches = actualValue.equals(expectedValue); if (!keyMatches) { - log.error("Key mismatch — expected: '{}', actual: '{}'", expectedKey, actualKey); + log.error("Key mismatch, expected: '{}', actual: '{}'", expectedKey, actualKey); } if (!valueMatches) { log.error( diff --git a/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java index 4a8d6e7..50a0f1b 100644 --- a/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java @@ -20,9 +20,10 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; -import java.util.List; import lombok.extern.slf4j.Slf4j; +import java.util.List; + /** * Asserts that the record key equals the expected key defined in {@link MatchContext#getMatchKey()}. * @@ -39,16 +40,24 @@ public MatchResult match(List> records, MatchContext cont } String expectedKey = context.getMatchKey(); - String actualKey = records.get(0).getKey(); + String actualKey = records.getFirst().getKey(); - if (expectedKey.equals(actualKey)) { - log.info("Record key matches expected key '{}'.", expectedKey); + if (context.getKeyMatchStrategy().matches(expectedKey, actualKey)) { + log.info( + "Record key matches expected key '{}' using {} strategy.", + expectedKey, + context.getKeyMatchStrategy()); return MatchResult.pass(expectedKey, actualKey); } - log.error("Record key mismatch — expected: '{}', actual: '{}'", expectedKey, actualKey); + log.error( + "Record key mismatch, expected: '{}', actual: '{}', using {} strategy.", + expectedKey, + actualKey, + context.getKeyMatchStrategy()); return MatchResult.fail( - "Record key does not match — expected: '" + expectedKey + "', actual: '" + actualKey + "'.", + "Record key does not match, expected: '" + expectedKey + "', actual: '" + actualKey + "', using " + + context.getKeyMatchStrategy() + " strategy.", expectedKey, actualKey); } diff --git a/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java b/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java index 95bc220..59897ff 100644 --- a/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java +++ b/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java @@ -15,39 +15,34 @@ */ package io.github.ktestify.io.kafka; -import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_FILE; -import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_XML; -import static org.junit.jupiter.api.Assertions.*; - import io.github.ktestify.config.ConfigBuilder; import io.github.ktestify.config.KtestifyConfig; import io.github.ktestify.exceptions.ConsumerException; import io.github.ktestify.io.kafka.impl.RawKafkaConsumer; import io.github.ktestify.models.Topic; import io.github.ktestify.tests.extentions.KafkaTestExtension; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Properties; -import java.util.UUID; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringSerializer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Properties; +import java.util.UUID; + +import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_FILE; +import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_XML; +import static org.junit.jupiter.api.Assertions.*; + /** * Integration tests for {@link RawKafkaConsumer}. * - *

Kafka broker is provided by {@link KafkaTestExtension}. Records are seeded via a plain {@link KafkaProducer} — NOT - * the project's RawKafkaProducer — keeping the consumer under test fully isolated from the producer implementation. + *

Kafka broker is provided by {@link KafkaTestExtension}. Records are seeded via a plain {@link KafkaProducer}: NOT + * the project's RawKafkaProducer: keeping the consumer under test fully isolated from the producer implementation. * *

Every test gets a fresh, uniquely-named topic and {@link KafkaRecordFetcher#clearMatchedRecords()} is called * before each test to prevent deduplication state leaking between tests. @@ -157,7 +152,7 @@ private ConsumerContext ctx() { // ========================================================================= @Nested - @DisplayName("Consume-only — no matcher") + @DisplayName("Consume-only: no matcher") class ConsumeOnly { @Test @@ -172,7 +167,7 @@ void returnsTrueWhenRecordPresent() throws Exception { @Test @DisplayName("throws ConsumerException when topic is empty and timeout elapses") void throwsWhenTopicEmpty() { - // Do NOT seed — consumer must time out + // Do NOT seed: consumer must time out ConsumerContext ctx = ConsumerContext.builder() .topic(outputTopic()) .consumer(KafkaClientFactory.createRawConsumer( @@ -190,7 +185,7 @@ void throwsWhenTopicEmpty() { // ========================================================================= @Nested - @DisplayName("File matching — METHOD_MATCH_FILE") + @DisplayName("File matching: METHOD_MATCH_FILE") class FileMatching { @Test @@ -245,7 +240,7 @@ void doesNotMatchFile() throws Exception { // ========================================================================= @Nested - @DisplayName("Key filter — expectedRecordKey") + @DisplayName("Key filter: expectedRecordKey") class KeyFilter { @Test @@ -273,7 +268,7 @@ void picksRecordWithMatchingKey() throws Exception { // ========================================================================= @Nested - @DisplayName("XML matching — METHOD_MATCH_XML") + @DisplayName("XML matching: METHOD_MATCH_XML") class XmlMatching { @Test @@ -301,7 +296,7 @@ void matchesXml() throws Exception { // ========================================================================= @Nested - @DisplayName("Batch consumption — isBatchConsumer(true)") + @DisplayName("Batch consumption: isBatchConsumer(true)") class BatchConsumption { /** The 4 payloads seeded in every test in this nested class. */ @@ -350,7 +345,7 @@ void throwsWhenNotEnoughRecords() throws Exception { .topic(outputTopic()) .consumer(KafkaClientFactory.createRawConsumer( KtestifyConfig.getOrLoad(), "raw-batch-short-" + UUID.randomUUID())) - .readTimeout(3_000L) // short timeout — we expect a failure + .readTimeout(3_000L) // short timeout: we expect a failure .consumerDeltaTime(60_000L) .isBatchConsumer(true) .batchSize(4) @@ -359,7 +354,7 @@ void throwsWhenNotEnoughRecords() throws Exception { } @Test - @DisplayName("all 4 records are registered as matched — a second consumer finds nothing") + @DisplayName("all 4 records are registered as matched: a second consumer finds nothing") void batchRecordsAreDeduplicated() throws Exception { seedBatch(); @@ -375,7 +370,7 @@ void batchRecordsAreDeduplicated() throws Exception { .build()) .call(); - // Second consumer — all records already matched, so it must time out + // Second consumer: all records already matched, so it must time out assertThrows( ConsumerException.class, () -> new RawKafkaConsumer(ConsumerContext.builder() @@ -392,12 +387,12 @@ void batchRecordsAreDeduplicated() throws Exception { } // ========================================================================= - // referenceTimestamp — pinned "now" fixes clock-drift across delayed fetches + // referenceTimestamp: pinned "now" fixes clock-drift across delayed fetches // (see https://github.com/ktestify/ktestify-cucumber/issues/38) // ========================================================================= @Nested - @DisplayName("referenceTimestamp — pinned 'now' avoids clock drift") + @DisplayName("referenceTimestamp: pinned 'now' avoids clock drift") class ReferenceTimestamp { /** Narrow enough that a few seconds of drift pushes the seek window past the seeded record. */ @@ -412,7 +407,7 @@ void recordIsMissedDueToClockDriftWithoutReferenceTimestamp() throws Exception { seedRecord("KEY-1", "{\"orderId\":\"ORD-DRIFT\"}"); // Simulate the delay a slow Cucumber step (or a previous DataTable row) would introduce - // before this consumer actually seeks — this is exactly the drift the maintainer described + // before this consumer actually seeks: this is exactly the drift the maintainer described // in issue #38. Thread.sleep(SIMULATED_STEP_DELAY_MS); @@ -422,7 +417,7 @@ void recordIsMissedDueToClockDriftWithoutReferenceTimestamp() throws Exception { KtestifyConfig.getOrLoad(), "drift-consumer-" + UUID.randomUUID())) .readTimeout(3_000L) .consumerDeltaTime(NARROW_DELTA_TIME_MS) - // No referenceTimestamp — "now" is resolved live, at seek time. + // No referenceTimestamp: "now" is resolved live, at seek time. .build(); assertThrows( @@ -467,7 +462,7 @@ void sequentialFetchesShareSameSeekWindow() throws Exception { seedRecord("KEY-1", "{\"orderId\":\"ORD-A\"}"); seedRecord("KEY-2", "{\"orderId\":\"ORD-B\"}"); - // First "row" — simulate a small delay before it runs. + // First "row": simulate a small delay before it runs. Thread.sleep(1_500L); boolean firstResult = new RawKafkaConsumer(ConsumerContext.builder() .topic(outputTopic()) @@ -480,7 +475,7 @@ void sequentialFetchesShareSameSeekWindow() throws Exception { .build()) .call(); - // Second "row" — additional delay elapses before it runs too. + // Second "row": additional delay elapses before it runs too. Thread.sleep(1_500L); boolean secondResult = new RawKafkaConsumer(ConsumerContext.builder() .topic(outputTopic()) diff --git a/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java b/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java new file mode 100644 index 0000000..938db12 --- /dev/null +++ b/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2026 Nil MALHOMME (malhomme.nil+oss@icloud.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ktestify.match; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link KeyMatchStrategy}. + * + * @since 1.1.1 + */ +@DisplayName("KeyMatchStrategy") +class KeyMatchStrategyTest { + + @Nested + @DisplayName("EXACT strategy") + class ExactStrategy { + + @ParameterizedTest + @CsvSource({"ORDER-1, ORDER-1", "key-123, key-123", "'', ''"}) + @DisplayName("matches when keys are exactly equal") + void matchesWhenEqual(String expected, String actual) { + assertTrue(KeyMatchStrategy.EXACT.matches(expected, actual)); + } + + @Test + @DisplayName("does not match when keys differ") + void doesNotMatchWhenDifferent() { + assertFalse(KeyMatchStrategy.EXACT.matches("ORDER-1", "ORDER-2")); + } + + @Test + @DisplayName("does not match when actual is null") + void doesNotMatchWhenActualIsNull() { + assertFalse(KeyMatchStrategy.EXACT.matches("ORDER-1", null)); + } + + @Test + @DisplayName("does not match when expected is null") + void doesNotMatchWhenExpectedIsNull() { + assertFalse(KeyMatchStrategy.EXACT.matches(null, "ORDER-1")); + } + } + + @Nested + @DisplayName("CONTAINS strategy") + class ContainsStrategy { + + @ParameterizedTest + @CsvSource({"ORD, ORDER-123", "123, ORDER-123", "ORDER, ORDER-123"}) + @DisplayName("matches when actual contains expected substring") + void matchesWhenContains(String expected, String actual) { + assertTrue(KeyMatchStrategy.CONTAINS.matches(expected, actual)); + } + + @Test + @DisplayName("does not match when actual does not contain expected") + void doesNotMatchWhenNotContains() { + assertFalse(KeyMatchStrategy.CONTAINS.matches("XYZ", "ORDER-123")); + } + + @Test + @DisplayName("does not match when actual is null") + void doesNotMatchWhenActualIsNull() { + assertFalse(KeyMatchStrategy.CONTAINS.matches("ORD", null)); + } + + @Test + @DisplayName("does not match when expected is null") + void doesNotMatchWhenExpectedIsNull() { + assertFalse(KeyMatchStrategy.CONTAINS.matches(null, "ORDER-123")); + } + } + + @Nested + @DisplayName("STARTS_WITH strategy") + class StartsWithStrategy { + + @ParameterizedTest + @CsvSource({"ORD, ORDER-123", "ORDER-, ORDER-123", "ORDER-1, ORDER-123"}) + @DisplayName("matches when actual starts with expected prefix") + void matchesWhenStartsWith(String expected, String actual) { + assertTrue(KeyMatchStrategy.STARTS_WITH.matches(expected, actual)); + } + + @Test + @DisplayName("does not match when actual does not start with expected") + void doesNotMatchWhenNotStartsWith() { + assertFalse(KeyMatchStrategy.STARTS_WITH.matches("USER-", "ORDER-123")); + } + + @Test + @DisplayName("does not match when actual is null") + void doesNotMatchWhenActualIsNull() { + assertFalse(KeyMatchStrategy.STARTS_WITH.matches("ORD", null)); + } + + @Test + @DisplayName("does not match when expected is null") + void doesNotMatchWhenExpectedIsNull() { + assertFalse(KeyMatchStrategy.STARTS_WITH.matches(null, "ORDER-123")); + } + } + + @Nested + @DisplayName("ENDS_WITH strategy") + class EndsWithStrategy { + + @ParameterizedTest + @CsvSource({"123, ORDER-123", "-123, ORDER-123", "R-123, ORDER-123"}) + @DisplayName("matches when actual ends with expected suffix") + void matchesWhenEndsWith(String expected, String actual) { + assertTrue(KeyMatchStrategy.ENDS_WITH.matches(expected, actual)); + } + + @Test + @DisplayName("does not match when actual does not end with expected") + void doesNotMatchWhenNotEndsWith() { + assertFalse(KeyMatchStrategy.ENDS_WITH.matches("456", "ORDER-123")); + } + + @Test + @DisplayName("does not match when actual is null") + void doesNotMatchWhenActualIsNull() { + assertFalse(KeyMatchStrategy.ENDS_WITH.matches("123", null)); + } + + @Test + @DisplayName("does not match when expected is null") + void doesNotMatchWhenExpectedIsNull() { + assertFalse(KeyMatchStrategy.ENDS_WITH.matches(null, "ORDER-123")); + } + } + + @Nested + @DisplayName("REGEX strategy") + class RegexStrategy { + + @ParameterizedTest + @CsvSource({"ORD-\\d+, ORD-123", "ORD-\\d{3}, ORD-123", ".*123, ORDER-123"}) + @DisplayName("matches when actual matches expected regex pattern") + void matchesWhenRegexMatches(String expected, String actual) { + assertTrue(KeyMatchStrategy.REGEX.matches(expected, actual)); + } + + @Test + @DisplayName("does not match when actual does not match regex") + void doesNotMatchWhenRegexDoesNotMatch() { + assertFalse(KeyMatchStrategy.REGEX.matches("ORD-\\d{6}", "ORDER-123")); + } + + @Test + @DisplayName("does not match when actual is null") + void doesNotMatchWhenActualIsNull() { + assertFalse(KeyMatchStrategy.REGEX.matches("ORD-\\d+", null)); + } + + @Test + @DisplayName("does not match when expected is null") + void doesNotMatchWhenExpectedIsNull() { + assertFalse(KeyMatchStrategy.REGEX.matches(null, "ORDER-123")); + } + } + + @Nested + @DisplayName("fromString parsing") + class FromStringParsing { + + @ParameterizedTest + @CsvSource({"exact, EXACT", "EXACT, EXACT", "Exact, EXACT"}) + @DisplayName("parses exact variations") + void parsesExact(String input, KeyMatchStrategy expected) { + assertEquals(expected, KeyMatchStrategy.fromString(input)); + } + + @ParameterizedTest + @CsvSource({"contains, CONTAINS", "CONTAINS, CONTAINS"}) + @DisplayName("parses contains variations") + void parsesContains(String input, KeyMatchStrategy expected) { + assertEquals(expected, KeyMatchStrategy.fromString(input)); + } + + @ParameterizedTest + @CsvSource({ + "starts_with, STARTS_WITH", + "starts-with, STARTS_WITH", + "STARTS-WITH, STARTS_WITH", + "STARTS WITH, STARTS_WITH" + }) + @DisplayName("parses starts_with variations including hyphens and spaces") + void parsesStartsWith(String input, KeyMatchStrategy expected) { + assertEquals(expected, KeyMatchStrategy.fromString(input)); + } + + @ParameterizedTest + @CsvSource({"ends_with, ENDS_WITH", "ends-with, ENDS_WITH", "ENDS-WITH, ENDS_WITH", "ENDS WITH, ENDS_WITH"}) + @DisplayName("parses ends_with variations including hyphens and spaces") + void parsesEndsWith(String input, KeyMatchStrategy expected) { + assertEquals(expected, KeyMatchStrategy.fromString(input)); + } + + @ParameterizedTest + @CsvSource({"regex, REGEX", "REGEX, REGEX"}) + @DisplayName("parses regex variations") + void parsesRegex(String input, KeyMatchStrategy expected) { + assertEquals(expected, KeyMatchStrategy.fromString(input)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "unknown", "invalid-strategy"}) + @DisplayName("defaults to EXACT for null, blank, or unrecognized values") + void defaultsToExactForUnrecognized(String input) { + assertEquals(KeyMatchStrategy.EXACT, KeyMatchStrategy.fromString(input)); + } + } +} diff --git a/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java b/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java index 34c6800..43dc060 100644 --- a/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java +++ b/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java @@ -15,16 +15,17 @@ */ package io.github.ktestify.match.impl; -import static io.github.ktestify.match.impl.MatcherTestSupport.*; -import static org.junit.jupiter.api.Assertions.*; - import io.github.ktestify.exceptions.ComparisonException; +import io.github.ktestify.match.KeyMatchStrategy; import io.github.ktestify.match.MatchContext; import io.github.ktestify.match.MatchResult; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static io.github.ktestify.match.impl.MatcherTestSupport.*; +import static org.junit.jupiter.api.Assertions.*; + @DisplayName("KeyRecordMatcher") class KeyRecordMatcherTest { @@ -87,4 +88,87 @@ void resultCarriesKeys() throws ComparisonException { assertEquals("ACTUAL", result.getActual()); } } + + @Nested + @DisplayName("Key match strategies") + class KeyStrategies { + + @Test + @DisplayName("STARTS_WITH passes when key starts with expected prefix") + void startsWithPasses() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("ORD-abc-123", "value"), ctxWithKeyAndStrategy("ORD-", KeyMatchStrategy.STARTS_WITH)); + assertTrue(result.isPassed()); + } + + @Test + @DisplayName("STARTS_WITH fails when key does not start with expected prefix") + void startsWithFails() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("USER-abc-123", "value"), ctxWithKeyAndStrategy("ORD-", KeyMatchStrategy.STARTS_WITH)); + assertFalse(result.isPassed()); + } + + @Test + @DisplayName("CONTAINS passes when key contains expected substring") + void containsPasses() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("order-ABC-123", "value"), ctxWithKeyAndStrategy("ABC", KeyMatchStrategy.CONTAINS)); + assertTrue(result.isPassed()); + } + + @Test + @DisplayName("CONTAINS fails when key does not contain expected substring") + void containsFails() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("order-XYZ-123", "value"), ctxWithKeyAndStrategy("ABC", KeyMatchStrategy.CONTAINS)); + assertFalse(result.isPassed()); + } + + @Test + @DisplayName("ENDS_WITH passes when key ends with expected suffix") + void endsWithPasses() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("ORD-abc-123", "value"), ctxWithKeyAndStrategy("-123", KeyMatchStrategy.ENDS_WITH)); + assertTrue(result.isPassed()); + } + + @Test + @DisplayName("ENDS_WITH fails when key does not end with expected suffix") + void endsWithFails() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("ORD-abc-456", "value"), ctxWithKeyAndStrategy("-123", KeyMatchStrategy.ENDS_WITH)); + assertFalse(result.isPassed()); + } + + @Test + @DisplayName("REGEX passes when key matches expected pattern") + void regexPasses() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("ORD-123456", "value"), ctxWithKeyAndStrategy("ORD-\\d{6}", KeyMatchStrategy.REGEX)); + assertTrue(result.isPassed()); + } + + @Test + @DisplayName("REGEX fails when key does not match expected pattern") + void regexFails() throws ComparisonException { + MatchResult result = matcher.match( + rawRecord("ORD-abc", "value"), ctxWithKeyAndStrategy("ORD-\\d{6}", KeyMatchStrategy.REGEX)); + assertFalse(result.isPassed()); + } + + @Test + @DisplayName("EXACT is the default when strategy is not set") + void exactIsDefault() throws ComparisonException { + MatchResult result = matcher.match(rawRecord("ORD-123", "value"), ctxWithKey("ORD-123")); + assertTrue(result.isPassed()); + } + + @Test + @DisplayName("EXACT fails for partial match (backward compatibility)") + void exactFailsForPartial() throws ComparisonException { + MatchResult result = matcher.match(rawRecord("ORD-123", "value"), ctxWithKey("ORD")); + assertFalse(result.isPassed()); + } + } } diff --git a/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java b/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java index 4ed40c3..5d3ab15 100644 --- a/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java +++ b/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java @@ -15,16 +15,18 @@ */ package io.github.ktestify.match.impl; +import io.github.ktestify.match.KeyMatchStrategy; import io.github.ktestify.match.MatchContext; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.utils.FileUtils; +import org.apache.avro.generic.GenericRecord; + import java.net.URL; import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; -import org.apache.avro.generic.GenericRecord; /** * Shared test helpers for {@link RecordMatcher} unit tests. Keeps each test class lean — no boilerplate record @@ -107,6 +109,14 @@ static MatchContext ctxWithKey(String matchKey) { return MatchContext.builder().matchKey(matchKey).build(); } + /** Builds a {@link MatchContext} with a match key and a key match strategy. */ + static MatchContext ctxWithKeyAndStrategy(String matchKey, KeyMatchStrategy strategy) { + return MatchContext.builder() + .matchKey(matchKey) + .keyMatchStrategy(strategy) + .build(); + } + /** Builds a {@link MatchContext} with a match key and an inline expected value. */ static MatchContext ctxWithKeyAndValue(String matchKey, String matchValue) { return MatchContext.builder().matchKey(matchKey).matchValue(matchValue).build(); From 759b96bc24846b35e6864839ce0dc56a286b1fc0 Mon Sep 17 00:00:00 2001 From: Nil MALHOMME Date: Thu, 1 Oct 2026 19:07:46 +0200 Subject: [PATCH 2/3] style: spotless --- .../io/kafka/AbstractKafkaConsumer.java | 3 +-- .../ktestify/io/kafka/ConsumerContext.java | 5 ++--- .../ktestify/io/kafka/KafkaRecordFetcher.java | 13 ++++++------- .../github/ktestify/match/MatchContext.java | 5 ++--- .../match/impl/AvroFileKeyRecordMatcher.java | 3 +-- .../match/impl/AvroKeyRecordMatcher.java | 3 +-- .../match/impl/FileKeyRecordMatcher.java | 3 +-- .../ktestify/match/impl/KeyRecordMatcher.java | 3 +-- .../io/kafka/RawKafkaConsumerTest.java | 19 +++++++++---------- .../ktestify/match/KeyMatchStrategyTest.java | 4 ++-- .../match/impl/KeyRecordMatcherTest.java | 6 +++--- .../match/impl/MatcherTestSupport.java | 3 +-- 12 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java b/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java index 066dd8c..6fddb89 100644 --- a/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java +++ b/src/main/java/io/github/ktestify/io/kafka/AbstractKafkaConsumer.java @@ -22,9 +22,8 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; -import lombok.extern.slf4j.Slf4j; - import java.util.List; +import lombok.extern.slf4j.Slf4j; /** * Thin coordinator that wires a {@link KafkaRecordFetcher} (transport) with a {@link RecordMatcher} (assertion) and diff --git a/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java b/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java index 1f7a643..47ff992 100644 --- a/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java +++ b/src/main/java/io/github/ktestify/io/kafka/ConsumerContext.java @@ -19,12 +19,11 @@ import io.github.ktestify.exceptions.ConsumerException; import io.github.ktestify.match.KeyMatchStrategy; import io.github.ktestify.models.Topic; -import lombok.Getter; -import org.apache.kafka.clients.consumer.Consumer; - import java.util.Collections; import java.util.List; import java.util.Map; +import lombok.Getter; +import org.apache.kafka.clients.consumer.Consumer; @Getter public final class ConsumerContext { diff --git a/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java b/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java index b76433f..9005cc0 100644 --- a/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java +++ b/src/main/java/io/github/ktestify/io/kafka/KafkaRecordFetcher.java @@ -15,12 +15,18 @@ */ package io.github.ktestify.io.kafka; +import static io.github.ktestify.constants.LogMessagesConstants.*; + import io.github.ktestify.config.FrameworkConfig; import io.github.ktestify.config.KtestifyConfig; import io.github.ktestify.exceptions.FetchException; import io.github.ktestify.io.core.RecordFetcher; import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.models.MatchedRecord; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -29,13 +35,6 @@ import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.WakeupException; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import static io.github.ktestify.constants.LogMessagesConstants.*; - /** * Kafka implementation of {@link RecordFetcher}. * diff --git a/src/main/java/io/github/ktestify/match/MatchContext.java b/src/main/java/io/github/ktestify/match/MatchContext.java index 5317ec4..edc55b1 100644 --- a/src/main/java/io/github/ktestify/match/MatchContext.java +++ b/src/main/java/io/github/ktestify/match/MatchContext.java @@ -15,12 +15,11 @@ */ package io.github.ktestify.match; -import lombok.Builder; -import lombok.Value; - import java.util.Collections; import java.util.List; import java.util.Map; +import lombok.Builder; +import lombok.Value; /** * Immutable value object that carries all configuration needed by a {@code RecordMatcher}. diff --git a/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java index 0ac9fb0..987d78b 100644 --- a/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/AvroFileKeyRecordMatcher.java @@ -23,11 +23,10 @@ import io.github.ktestify.utils.FileUtils; import io.github.ktestify.utils.serdes.AvroDeserializer; import io.github.ktestify.utils.serdes.AvroUtils; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.avro.generic.GenericRecord; -import java.util.List; - /** * Asserts both the record key and Avro value against a configured expected key and expected JSON * file. diff --git a/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java index e829be1..43c0d55 100644 --- a/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/AvroKeyRecordMatcher.java @@ -20,11 +20,10 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.avro.generic.GenericRecord; -import java.util.List; - /** * Asserts that the Avro record key equals the expected key in {@link MatchContext#getMatchKey()}. * diff --git a/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java index c754f37..bcf1fa1 100644 --- a/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/FileKeyRecordMatcher.java @@ -22,9 +22,8 @@ import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.utils.FileUtils; import io.github.ktestify.utils.StringDiffUtils; -import lombok.extern.slf4j.Slf4j; - import java.util.List; +import lombok.extern.slf4j.Slf4j; /** * Compares both the record key and value against a configured expected key and expected file content. diff --git a/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java b/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java index 50a0f1b..2b03527 100644 --- a/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java +++ b/src/main/java/io/github/ktestify/match/impl/KeyRecordMatcher.java @@ -20,9 +20,8 @@ import io.github.ktestify.match.MatchResult; import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; -import lombok.extern.slf4j.Slf4j; - import java.util.List; +import lombok.extern.slf4j.Slf4j; /** * Asserts that the record key equals the expected key defined in {@link MatchContext#getMatchKey()}. diff --git a/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java b/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java index 59897ff..17a6442 100644 --- a/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java +++ b/src/test/java/io/github/ktestify/io/kafka/RawKafkaConsumerTest.java @@ -15,12 +15,21 @@ */ package io.github.ktestify.io.kafka; +import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_FILE; +import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_XML; +import static org.junit.jupiter.api.Assertions.*; + import io.github.ktestify.config.ConfigBuilder; import io.github.ktestify.config.KtestifyConfig; import io.github.ktestify.exceptions.ConsumerException; import io.github.ktestify.io.kafka.impl.RawKafkaConsumer; import io.github.ktestify.models.Topic; import io.github.ktestify.tests.extentions.KafkaTestExtension; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Properties; +import java.util.UUID; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; @@ -28,16 +37,6 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Properties; -import java.util.UUID; - -import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_FILE; -import static io.github.ktestify.match.RecordMatcherFactory.METHOD_MATCH_XML; -import static org.junit.jupiter.api.Assertions.*; - /** * Integration tests for {@link RawKafkaConsumer}. * diff --git a/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java b/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java index 938db12..129bfd4 100644 --- a/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java +++ b/src/test/java/io/github/ktestify/match/KeyMatchStrategyTest.java @@ -15,6 +15,8 @@ */ package io.github.ktestify.match; +import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -23,8 +25,6 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - /** * Unit tests for {@link KeyMatchStrategy}. * diff --git a/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java b/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java index 43dc060..f79e517 100644 --- a/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java +++ b/src/test/java/io/github/ktestify/match/impl/KeyRecordMatcherTest.java @@ -15,6 +15,9 @@ */ package io.github.ktestify.match.impl; +import static io.github.ktestify.match.impl.MatcherTestSupport.*; +import static org.junit.jupiter.api.Assertions.*; + import io.github.ktestify.exceptions.ComparisonException; import io.github.ktestify.match.KeyMatchStrategy; import io.github.ktestify.match.MatchContext; @@ -23,9 +26,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static io.github.ktestify.match.impl.MatcherTestSupport.*; -import static org.junit.jupiter.api.Assertions.*; - @DisplayName("KeyRecordMatcher") class KeyRecordMatcherTest { diff --git a/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java b/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java index 5d3ab15..cacb387 100644 --- a/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java +++ b/src/test/java/io/github/ktestify/match/impl/MatcherTestSupport.java @@ -20,13 +20,12 @@ import io.github.ktestify.match.RecordMatcher; import io.github.ktestify.models.ConsumedRecord; import io.github.ktestify.utils.FileUtils; -import org.apache.avro.generic.GenericRecord; - import java.net.URL; import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.avro.generic.GenericRecord; /** * Shared test helpers for {@link RecordMatcher} unit tests. Keeps each test class lean — no boilerplate record From 3c8e3fa65298ac11605b087012ca8ffc506a44c6 Mon Sep 17 00:00:00 2001 From: Nil MALHOMME Date: Thu, 1 Oct 2026 19:22:04 +0200 Subject: [PATCH 3/3] style: spotless apply --- .../github/ktestify/match/KeyMatchStrategy.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java b/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java index 9de1c70..867ec41 100644 --- a/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java +++ b/src/main/java/io/github/ktestify/match/KeyMatchStrategy.java @@ -29,7 +29,7 @@ *

The default strategy is {@link #EXACT}, which preserves the original {@code String.equals()} behavior. Other * strategies allow matching dynamically generated keys by prefix, suffix, substring, or regular expression. * - * @since 1.1.1 + * @since 1.1.4 */ public enum KeyMatchStrategy { @@ -39,7 +39,7 @@ public enum KeyMatchStrategy { *

This is the default and preserves backward compatibility for feature files that do not specify a * {@code keyMatchStrategy} column. * - * @since 1.1.1 + * @since 1.1.4 */ EXACT { @Override @@ -53,7 +53,7 @@ public boolean matches(String expected, String actual) { * *

Useful when the record key contains a known fragment embedded in a larger dynamically generated value. * - * @since 1.1.1 + * @since 1.1.4 */ CONTAINS { @Override @@ -68,7 +68,7 @@ public boolean matches(String expected, String actual) { *

Useful when the record key starts with a known prefix followed by a dynamically generated suffix (e.g. * {@code ORD-}). * - * @since 1.1.1 + * @since 1.1.4 */ STARTS_WITH { @Override @@ -82,7 +82,7 @@ public boolean matches(String expected, String actual) { * *

Useful when the record key ends with a known suffix preceded by a dynamically generated prefix. * - * @since 1.1.1 + * @since 1.1.4 */ ENDS_WITH { @Override @@ -97,7 +97,7 @@ public boolean matches(String expected, String actual) { *

The {@code expected} string is interpreted as a Java regular expression. Useful for arbitrary patterns such as * {@code ORD-\d{6}} that cannot be expressed with prefix, suffix, or substring matching. * - * @since 1.1.1 + * @since 1.1.4 */ REGEX { @Override @@ -112,7 +112,7 @@ public boolean matches(String expected, String actual) { * @param expected the expected key value (or pattern for {@link #REGEX}) * @param actual the actual record key, may be {@code null} when the Kafka record has no key * @return {@code true} if the actual key matches according to this strategy - * @since 1.1.1 + * @since 1.1.4 */ public abstract boolean matches(String expected, String actual); @@ -124,7 +124,7 @@ public boolean matches(String expected, String actual) { * * @param value the raw column value, may be {@code null} or blank * @return the parsed strategy, or {@link #EXACT} when the value is {@code null}, blank, or unrecognized - * @since 1.1.1 + * @since 1.1.4 */ public static KeyMatchStrategy fromString(String value) { if (value == null || value.isBlank()) {