# Batch Assertions

> Assert multiple Kafka records in a single step, raw and Avro batch matchers.

Source: https://docs.ktestify.xyz/docs/write-tests/assertions/batch-assertions

Batch steps consume multiple records from an output topic in a single step and match each one against a corresponding expected file. By default, records and files are matched **positionally** (`record[0]` matches `file[0]`, `record[1]` matches `file[1]`, and so on). You can set `batchMatchStrategy` to `lazy` for order-independent 1-to-1 matching without replacement.

---

## Raw batch: `Then expected records from files`

```gherkin
Then expected records from files
  | topicAlias | expectedRecordsCount | files                            | expectedRecordKeys | batchMatchStrategy | consumerReadTimeout |
  | orders-out | 3                    | exp-1.json,exp-2.json,exp-3.json | k1,k2,k3           | positional         | 60                  |
```

| Column | Type | Required | Description |
|---|---|---|---|
| `topicAlias` | `string` | ✅ | Alias of a declared output topic |
| `expectedRecordsCount` | `integer` | ✅ | Exact number of records to collect before asserting |
| `files` | `string` | ✅ | Comma-separated list of expected file paths (relative to assets dir), one per record |
| `batchMatchStrategy` | `string` | ❌ | How collected records are matched against `files` (and `expectedRecordKeys`): `positional` (default) or `lazy` |
| `expectedRecordKeys` | `string` | ❌ | Comma-separated list of expected record keys (one per file in `files`, or a single key shared across all records in the batch), pre-filtered during polling and asserted alongside each file using `batchMatchStrategy` and `keyMatchStrategy` |
| `expectedRecordKey` | `string` | ❌ | Optional single key filter applied while collecting records |
| `keyMatchStrategy` | `string` | ❌ | How `expectedRecordKeys` or `expectedRecordKey` is matched (`exact`, `contains`, `starts_with`, `ends_with`, `regex`) |
| `consumerReadTimeout` | `integer` | ❌ | Max seconds to wait for **all** records |
| `consumerDeltaTime` | `integer` | ❌ | How far back (seconds) to seek |

---

## Avro batch: `Then expected records from files based on schema`

```gherkin
Then expected records from files based on schema
  | topicAlias | expectedRecordsCount | files                    | expectedRecordKeys | excludedKeys | batchMatchStrategy |
  | orders-out | 2                    | exp-1.json,exp-2.json    | k1,k2              | ts,eventId   | lazy               |
```

| Column | Type | Required | Description |
|---|---|---|---|
| `topicAlias` | `string` | ✅ | Alias of a declared output topic |
| `expectedRecordsCount` | `integer` | ✅ | Exact number of records to collect |
| `files` | `string` | ✅ | Comma-separated list of expected JSON file paths |
| `excludedKeys` | `string` | ❌ | Comma-separated Avro field names to exclude from all records |
| `batchMatchStrategy` | `string` | ❌ | How collected records are matched against `files` (and `expectedRecordKeys`): `positional` (default) or `lazy` |
| `expectedRecordKeys` | `string` | ❌ | Comma-separated list of expected record keys (one per file in `files`, or a single key shared across all records in the batch), pre-filtered during polling and asserted alongside each file using `batchMatchStrategy` and `keyMatchStrategy` |
| `expectedRecordKey` | `string` | ❌ | Optional single key filter applied while collecting records |
| `keyMatchStrategy` | `string` | ❌ | How `expectedRecordKeys` or `expectedRecordKey` is matched (`exact`, `contains`, `starts_with`, `ends_with`, `regex`) |
| `consumerReadTimeout` | `integer` | ❌ | Max seconds to wait for all records |
| `consumerDeltaTime` | `integer` | ❌ | How far back (seconds) to seek |

---

## Timeout behaviour

- If the consumer collects **fewer than `expectedRecordsCount`** records before the timeout, the step fails with:
  > `Timed out after Nms waiting for 4 record(s), only 2 collected.`
- Increase `consumerReadTimeout` if your pipeline produces output slowly.

---

## Single-row only

Unlike single-record assertion steps (see [Multi-row DataTables](../advanced/multi-row-datatables)), batch steps accept **exactly one DataTable row**. That one row already describes an any-to-all match across `expectedRecordsCount` records collected from a single consumer call, adding more rows would be ambiguous, so a multi-row DataTable here is rejected immediately:

```
This step only supports a single DataTable row but 2 rows were provided.
```

If you need several independent batches, or several unrelated single records against the same topic, use separate step invocations, or the multi-row single-record assertion steps ([raw matchers →](raw-matchers), [Avro matchers →](avro-matchers)) instead.

---

## Batch matching strategies (`batchMatchStrategy`)

| Strategy | Accepted DataTable values | Behaviour |
|---|---|---|
| `POSITIONAL` | `positional`, `ordered`, `index`, `strict` | Default. Record at index `i` must match file at index `i` (and `expectedRecordKeys[i]` when set). |
| `LAZY` | `lazy`, `fuzzy`, `unordered`, `any_order`, `any-order` | Order-independent 1-to-1 matching without replacement. Each consumed record claims the first unmatched expected file (and corresponding `expectedRecordKeys[j]` when set) whose content and key match. |

```gherkin
# Lazy / unordered: records arriving as (3, 1, 2) still match files listed as (1, 2, 3)
Then expected records from files
  | topicAlias | expectedRecordsCount | files                            | expectedRecordKeys | batchMatchStrategy | consumerReadTimeout | consumerDeltaTime |
  | orders-out | 3                    | exp-1.json,exp-2.json,exp-3.json | k1,k2,k3           | lazy               | 30                  | 60                |
```

If your pipeline writes across multiple partitions and does not guarantee record order, set `batchMatchStrategy` to `lazy` (or use individual assertion steps per record with `expectedRecordKey` filtering).

---

## See also

- [Raw matchers →](raw-matchers)
- [Avro matchers →](avro-matchers)
- [Batch testing guide →](../advanced/batch-testing)
- [Timeout tuning →](../advanced/timeout-tuning)
- [Multi-row DataTables →](../advanced/multi-row-datatables)
