Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/extend/core-concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class MatchContext {
boolean strictMatching;
String matchKey;
String matchValue;
KeyMatchStrategy keyMatchStrategy; // defaults to EXACT, controls how expectedRecordKey is matched
Map<String,String> expectedAttributes; // since 1.1.1, defaults to emptyMap(), used by AttributeRecordMatcher

// Convenience, for single-record matchers
Expand All @@ -68,6 +69,8 @@ public class MatchContext {

`expectedAttributes` holds the key/value pairs to assert against a record's `attributes` map, for example `{"statusCode": "200"}`. It follows the same convention as `excludedFields`: both default to an empty collection rather than null, and matchers check `isEmpty()` instead of null. The default `buildMatchContext()` in `AbstractKafkaConsumer` sets `strictMatching` to `false` and leaves `matchKey`, `matchValue`, and `expectedAttributes` unset.

`keyMatchStrategy` controls how `expectedRecordKey` is matched against the actual record key. It defaults to `KeyMatchStrategy.EXACT` for backward compatibility. Available strategies: `EXACT`, `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `REGEX`. The strategy is applied at two layers: the fetch-time pre-filter in `KafkaRecordFetcher.passesKeyFilter()` (to skip non-matching records during polling) and the assertion-time key matchers (`KeyRecordMatcher`, `FileKeyRecordMatcher`, `AvroKeyRecordMatcher`, `AvroFileKeyRecordMatcher`).

---

## `MatchResult`
Expand Down Expand Up @@ -104,6 +107,7 @@ Key fields:
| `matchFilePaths` | `List<String>` | Expected file paths (single or batch) |
| `excludedFields` | `List<String>` | Field names to ignore in comparison |
| `expectedRecordKey` | `String` | Key filter, the record must match this key |
| `keyMatchStrategy` | `KeyMatchStrategy` | How `expectedRecordKey` is matched: `EXACT` (default), `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `REGEX` |
| `readTimeout` | `long` | Milliseconds |
| `consumerDeltaTime` | `long` | Milliseconds (DataTable seconds × 1000) |
| `isBatchConsumer` | `boolean` | Enables batch fetch mode |
Expand Down Expand Up @@ -185,4 +189,3 @@ All file reads go through `FileUtils.getFileContent(path)`, which transparently
3. No changes needed to `FileUtils` or `DynamicVariableProcessor`.

See [Dynamic variables →](../write-tests/dynamic-variables) for the full list of built-in types.

14 changes: 7 additions & 7 deletions docs/extend/matchers/built-in-matchers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,17 @@ Compares the record value character-for-character against a file's content after

### `FileKeyRecordMatcher`

Asserts both the record **key** and **value**. The expected file must contain the key on line 1 and the value from line 2 onwards.
Asserts both the record **key** and **value**. The expected file must contain the key on line 1 and the value from line 2 onwards. Key matching uses the `KeyMatchStrategy` from `MatchContext` (defaults to `EXACT`).

- **MatchContext fields used:** `matchFilePaths.get(0)`
- **MatchContext fields used:** `matchFilePaths.get(0)`, `keyMatchStrategy`

---

### `KeyRecordMatcher`

Asserts only the record **key**, the value is not evaluated.
Asserts only the record **key**, the value is not evaluated. Key matching uses the `KeyMatchStrategy` from `MatchContext` (defaults to `EXACT`).

- **MatchContext fields used:** `matchKey` (falls back to `matchFilePaths.get(0)` if blank)
- **MatchContext fields used:** `matchKey` (falls back to `matchFilePaths.get(0)` if blank), `keyMatchStrategy`

---

Expand Down Expand Up @@ -116,13 +116,13 @@ Avro equivalent of `FileRecordMatcher`. Converts `GenericRecord` → JSON string

### `AvroFileKeyRecordMatcher`

Avro equivalent of `FileKeyRecordMatcher`. Asserts key + Avro-serialised JSON value.
Avro equivalent of `FileKeyRecordMatcher`. Asserts key + Avro-serialised JSON value. Key matching uses the `KeyMatchStrategy` from `MatchContext` (defaults to `EXACT`).

---

### `AvroKeyRecordMatcher`

Avro equivalent of `KeyRecordMatcher`. Key-only assertion; Avro value ignored.
Avro equivalent of `KeyRecordMatcher`. Key-only assertion; Avro value ignored. Key matching uses the `KeyMatchStrategy` from `MatchContext` (defaults to `EXACT`).

---

Expand All @@ -139,4 +139,4 @@ Avro equivalent of `FieldsRecordMatcher`. Extracts a character range from the JS
3. XML/XPath matchers throw `ConsumerException` when called via `RecordMatcherFactory.forAvro()`.
4. `NoOpRecordMatcher` is the default when no match method is configured.
5. `expectedAttributes` defaults to `Collections.emptyMap()`, `AttributeRecordMatcher` checks `isEmpty()`, never `null`, same convention as `excludedFields`.

6. `keyMatchStrategy` defaults to `KeyMatchStrategy.EXACT`, key matchers (`KeyRecordMatcher`, `FileKeyRecordMatcher`, `AvroKeyRecordMatcher`, `AvroFileKeyRecordMatcher`) and the fetch-time key filter (`KafkaRecordFetcher.passesKeyFilter()`) both delegate to `KeyMatchStrategy.matches(expected, actual)`. Available strategies: `EXACT`, `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `REGEX`.
65 changes: 55 additions & 10 deletions docs/write-tests/step-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,16 @@ Raw file match, full value equality.

```gherkin
Then expected record from file
| topicAlias | file | expectedRecordKey | consumerReadTimeout | consumerDeltaTime |
| orders-out | expected.json | key-001 | 30 | 60 |
| topicAlias | file | expectedRecordKey | keyMatchStrategy | consumerReadTimeout | consumerDeltaTime |
| orders-out | expected.json | key-001 | exact | 30 | 60 |
```

| Column | Type | Required | Default | Description |
|---|---|---|---|---|
| `topicAlias` | `string` | ✅ | none | Output topic alias |
| `file` | `string` | ✅ | none | Expected file path |
| `expectedRecordKey` | `string` | ❌ | none | Filter by record key |
| `keyMatchStrategy` | `string` | ❌ | `exact` | Key matching strategy: `exact`, `contains`, `starts_with`, `ends_with`, `regex` |
| `consumerReadTimeout` | `integer` | ❌ | Config (30) | Seconds to wait |
| `consumerDeltaTime` | `integer` | ❌ | Config (60) | Seconds to seek back |

Expand Down Expand Up @@ -247,8 +248,8 @@ Structural XML comparison with optional element exclusions.

```gherkin
Then expected record from file based on XML
| topicAlias | file | excludedElements | expectedRecordKey |
| orders-out | expected.xml | ns:CreationDateTime | |
| topicAlias | file | excludedElements | expectedRecordKey | keyMatchStrategy |
| orders-out | expected.xml | ns:CreationDateTime | | |
```

| Column | Type | Required | Default | Description |
Expand All @@ -257,6 +258,7 @@ Then expected record from file based on XML
| `file` | `string` | ✅ | none | Expected XML file |
| `excludedElements` | `string` | ❌ | none | Comma-separated element names to skip |
| `expectedRecordKey` | `string` | ❌ | none | Filter by record key |
| `keyMatchStrategy` | `string` | ❌ | `exact` | Key matching strategy: `exact`, `contains`, `starts_with`, `ends_with`, `regex` |
| `consumerReadTimeout` | `integer` | ❌ | Config (30) | Seconds to wait |
| `consumerDeltaTime` | `integer` | ❌ | Config (60) | Seconds to seek back |

Expand Down Expand Up @@ -286,8 +288,8 @@ Avro → JSON file match with optional field exclusions.

```gherkin
Then expected record from file based on schema
| topicAlias | file | excludedKeys | expectedRecordKey | consumerReadTimeout |
| orders-out | expected.json | createdAt,eventId | ord-001 | 30 |
| topicAlias | file | excludedKeys | expectedRecordKey | keyMatchStrategy | consumerReadTimeout |
| orders-out | expected.json | createdAt,eventId | ord-001 | exact | 30 |
```

| Column | Type | Required | Default | Description |
Expand All @@ -296,6 +298,7 @@ Then expected record from file based on schema
| `file` | `string` | ✅ | none | Expected JSON file |
| `excludedKeys` | `string` | ❌ | none | Comma-separated Avro field names to exclude |
| `expectedRecordKey` | `string` | ❌ | none | Filter by record key |
| `keyMatchStrategy` | `string` | ❌ | `exact` | Key matching strategy: `exact`, `contains`, `starts_with`, `ends_with`, `regex` |
| `consumerReadTimeout` | `integer` | ❌ | Config (30) | Seconds to wait |
| `consumerDeltaTime` | `integer` | ❌ | Config (60) | Seconds to seek back |

Expand Down Expand Up @@ -382,14 +385,16 @@ Positive watcher, asserts at least one record is present.

```gherkin
And record should appear in topic
| topicAlias | topicType | consumerReadTimeout | consumerDeltaTime |
| orders-out | raw | 15 | 60 |
| topicAlias | topicType | expectedRecordKey | keyMatchStrategy | consumerReadTimeout | consumerDeltaTime |
| orders-out | raw | order- | starts_with | 15 | 60 |
```

| Column | Type | Required | Default | Description |
|---|---|---|---|---|
| `topicAlias` | `string` | ✅ | none | Output topic alias |
| `topicType` | `string` | ✅ | none | `raw` or `avro` |
| `expectedRecordKey` | `string` | ❌ | none | Filter by record key |
| `keyMatchStrategy` | `string` | ❌ | `exact` | Key matching strategy: `exact`, `contains`, `starts_with`, `ends_with`, `regex` |
| `consumerReadTimeout` | `integer` | ❌ | Config (30) | Seconds to wait |
| `consumerDeltaTime` | `integer` | ❌ | Config (60) | Seconds to seek back |

Expand All @@ -401,8 +406,8 @@ Negative watcher, asserts no record appears within the timeout.

```gherkin
And record should not appear in topic
| topicAlias | topicType | consumerReadTimeout | consumerDeltaTime |
| orders-out | raw | 10 | 30 |
| topicAlias | topicType | expectedRecordKey | keyMatchStrategy | consumerReadTimeout | consumerDeltaTime |
| orders-out | raw | non-existent | exact | 10 | 30 |
```

Same columns as `record should appear in topic`.
Expand All @@ -424,6 +429,46 @@ Same columns as `record should appear in topic`.

---

## Key matching strategies

When a step includes an `expectedRecordKey` column, you can optionally specify a `keyMatchStrategy` column to control how the expected key is matched against the actual record key. This is useful when record keys are dynamically generated (UUIDs, prefixed keys, timestamps) and an exact match is too restrictive.

| Strategy | DataTable value | Behaviour | Example |
|---|---|---|---|
| `EXACT` | `exact` | Exact string equality (default) | `"order-001"` matches `"order-001"` |
| `CONTAINS` | `contains` | Expected key is a substring of the actual key | `"order"` matches `"order-001"` |
| `STARTS_WITH` | `starts_with` | Actual key starts with the expected key | `"order-"` matches `"order-001"` |
| `ENDS_WITH` | `ends_with` | Actual key ends with the expected key | `"001"` matches `"order-001"` |
| `REGEX` | `regex` | Actual key matches the expected Java regex pattern | `"order-\\d+"` matches `"order-001"` |

:::tip[Parsing]
The `keyMatchStrategy` value is case-insensitive and tolerant of hyphens, underscores, and spaces. For example, `starts_with`, `starts-with`, `starts_with`, `StartsWith`, and `starts with` all resolve to `STARTS_WITH`. Unrecognised values default to `EXACT`.
:::

:::warning[Topic isolation]
When using non-exact strategies (`contains`, `regex`, `starts_with`), ensure your match pattern is specific enough to uniquely identify the intended record. Kafka topics may contain records from prior scenarios in the same feature file, and a broad pattern (e.g. `contains "key"`) may match a decoy record first, causing value mismatches.
:::

### Example: matching a UUID-prefixed key

```gherkin
# Record key is "550e8400-e29b-41d4-a716-446655440000"
Then expected record from file
| topicAlias | file | expectedRecordKey | keyMatchStrategy | consumerReadTimeout |
| orders-out | expected.json | 550e8400 | starts_with | 30 |
```

### Example: matching a dynamic key with regex

```gherkin
# Record key is "ORD-2026-000001"
Then expected record from file
| topicAlias | file | expectedRecordKey | keyMatchStrategy | consumerReadTimeout |
| orders-out | expected.json | ORD-\d{4}-\d{6} | regex | 30 |
```

---

## See also

- [Multi-row DataTables →](advanced/multi-row-datatables)
Expand Down
Loading