Assertions

Assert on your step's response, inter-service traffic, and console logs.

Each step can have an assertions array of blocks. Block type is determined by shape — there are three kinds:

Self block

Asserts on the step's own response (HTTP response or DB query result). A self block has no match field.

{
  "assertions": [
    { "path": "$.response.status", "operator": "eq", "value": 201 },
    { "path": "$.response.body.id", "operator": "exists" },
    { "path": "$.response.body.name", "operator": "eq", "value": "{{userName}}" },
    { "path": "$.response.headers.content-type", "operator": "contains", "value": "application/json" },
    { "path": "$.responseTime", "operator": "lt", "value": 500 }
  ]
}

Match block

Asserts on inter-service traffic captured by the interceptor sidecar. A match block uses the match field with a path to select the data source and where clauses to filter entries.

{
  "match": {
    "path": "$.traffic",
    "where": [
      { "path": "$$.origin", "operator": "eq", "value": "api-gateway" },
      { "path": "$$.request.method", "operator": "eq", "value": "POST" },
      { "path": "$$.request.url", "operator": "contains", "value": "user-service/api/users" }
    ],
    "count": 1
  },
  "assertions": [
    { "path": "$.match.request.body.email", "operator": "eq", "value": "{{email}}" },
    { "path": "$.match.response.status", "operator": "eq", "value": 201 },
    { "path": "$.match.response.body.id", "operator": "exists" }
  ]
}

Scoped per test. $.traffic, $.consoleLogs, $.dbLogs, and $.messageLogs only include entries captured during the current test's steps. Traffic from one test is not visible in another test within the same definition. If you need to assert on traffic triggered by a previous step, keep the assertion in the same test.

Match fields

FieldTypeRequiredDescription
pathstringYesData source to query: $.traffic for HTTP traffic, $.consoleLogs for console output, $.messageLogs for broker messages, $.dbLogs for database queries
wherearrayNoFilter entries. Each clause uses $$. prefix to reference fields on the matched entry
countinteger or objectNoAssert how many entries match. An integer asserts an exact count. An object { "operator": "gte", "value": 1 } supports range operators (eq, gt, gte, lt, lte). If omitted, at least one match is expected.

Where clauses

Each where clause filters entries using $$.-prefixed paths that reference the candidate entry's fields. All clauses must pass for an entry to match (AND logic). All support {{variables}}:

"where": [
  { "path": "$$.origin", "operator": "eq", "value": "api-gateway" },
  { "path": "$$.request.method", "operator": "eq", "value": "POST" },
  { "path": "$$.request.url", "operator": "contains", "value": "/api/users" }
]

You can use combinators for complex filtering:

"where": [
  { "or": [
    { "path": "$$.request.method", "operator": "eq", "value": "POST" },
    { "path": "$$.request.method", "operator": "eq", "value": "PUT" }
  ]},
  { "not": { "path": "$$.request.url", "operator": "contains", "value": "/health" } }
]

Assertion paths inside match blocks

Inside a match block, assertion paths reference the matched entry via $.match.* (the first/default match) or $.lastMatch.* (the last match):

"assertions": [
  { "path": "$.match.response.status", "operator": "eq", "value": 200 },
  { "path": "$.match.request.body.name", "operator": "exists" }
]

Iterating over multiple matches

To assert on each matching entry individually, use a forEach block:

{
  "match": {
    "path": "$.traffic",
    "where": [
      { "path": "$$.origin", "operator": "eq", "value": "api-gateway" }
    ]
  },
  "forEach": {
    "items": "$.matches",
    "as": "entry",
    "assertions": [
      { "path": "$.variables.entry.response.status", "operator": "eq", "value": 200 }
    ]
  }
}

Console log block

Asserts on console output from a specific service during the step's execution. Console assertions use the same match structure with path: "$.consoleLogs":

{
  "match": {
    "path": "$.consoleLogs",
    "where": [
      { "path": "$$.service", "operator": "eq", "value": "user-service" },
      { "path": "$$.level", "operator": "eq", "value": "INFO" },
      { "path": "$$.message", "operator": "contains", "value": "User created" }
    ],
    "count": 1
  }
}

To assert zero errors from a service:

{
  "match": {
    "path": "$.consoleLogs",
    "where": [
      { "path": "$$.service", "operator": "eq", "value": "user-service" },
      { "path": "$$.level", "operator": "eq", "value": "ERROR" }
    ],
    "count": 0
  }
}

To assert on message content with regex:

{
  "match": {
    "path": "$.consoleLogs",
    "where": [
      { "path": "$$.service", "operator": "eq", "value": "user-service" },
      { "path": "$$.message", "operator": "matches", "value": "Processing order \\d+" }
    ],
    "count": 1
  }
}

Console log where fields

PathTypeDescription
$$.servicestringService name that emitted the log
$$.levelenumLog level: INFO, WARN, ERROR, DEBUG
$$.messagestringLog message content

Message log block

Asserts on broker messages (AMQP or Kafka) captured during the step. Uses match with path: "$.messageLogs":

{
  "match": {
    "path": "$.messageLogs",
    "where": [
      { "path": "$$.operation", "operator": "eq", "value": "produce" },
      { "path": "$$.brokerType", "operator": "eq", "value": "kafka" },
      { "path": "$$.topic", "operator": "eq", "value": "order-events" }
    ],
    "count": 1
  },
  "assertions": [
    { "path": "$.match.body.orderId", "operator": "eq", "value": 42 }
  ]
}

Message log where fields

PathTypeDescription
$$.brokerstringBroker item name
$$.brokerTypeenum"amqp" or "kafka"
$$.operationenum"publish" / "deliver" (AMQP) or "produce" / "consume" (Kafka)
$$.bodyanyMessage body (parsed JSON or raw string)
$$.exchangestringAMQP exchange name
$$.routingKeystringAMQP routing key
$$.topicstringKafka topic
$$.partitionintegerKafka partition index
$$.keystring/objectKafka message key
$$.offsetintegerKafka message offset (consume only)

Assertion paths

All assertion and extract paths use the unified root context. Paths must start with $. followed by the document root field.

HTTP responses (self block or match block)

PathDescription
$.response.statusHTTP status code (integer)
$.response.bodyEntire response body
$.response.body.fieldTop-level field in response body
$.response.body.nested.fieldNested field (dot notation)
$.response.body[0].fieldArray element access
$.response.headers.header-nameResponse header (case-insensitive)
$.request.methodHTTP method of the request
$.request.bodyEntire request body
$.request.body.fieldField in request body
$.request.headers.header-nameRequest header (case-insensitive)
$.responseTimeResponse time in milliseconds

Database query results (self block only)

PathDescription
$.response.successBoolean — did the query succeed?
$.response.dataArray of result rows
$.response.data[0].columnSpecific column from a result row
$.response.rowsAffectedNumber of affected rows (integer)
$.response.errorError message if query failed

Operators

OperatorValue required?Value typeDescription
eqYesanyEquality with coercion: numeric (1 == "1") and boolean (true == "TRUE"); case-sensitive for non-boolean strings
eqIgnoreCaseYesanyEquality, case-insensitive for strings
neYesanyNot equal (inverse of eq)
gtYesnumberGreater than
gteYesnumberGreater than or equal
ltYesnumberLess than
lteYesnumberLess than or equal
containsYesanyUnified containment: substring match for strings, element check for arrays, key check for objects (case-sensitive)
notContainsYesanyInverse of contains (case-sensitive)
containsIgnoreCaseYesstringSubstring match (case-insensitive)
notContainsIgnoreCaseYesstringSubstring does NOT match (case-insensitive)
matchesYesstring (regex)Regular expression match
existsNoValue exists (defined and not null)
notExistsNoValue does not exist
inYesarrayValue is in the given array
notInYesarrayValue is NOT in the given array
isEmptyNoValue is empty/null/undefined/empty array/empty object
notEmptyNoValue is not empty

Type coercion and edge cases

The eq and ne operators use loose comparison with automatic type coercion. Understanding these rules helps avoid surprising results, especially with typed variables.

How eq compares values

eq checks three things in order:

  1. Exact match — identical type and value (e.g. 42 == 42, "hello" == "hello")
  2. Boolean coercion — if both sides can be interpreted as booleans, compare as booleans. The string "true" (any casing) coerces to true, "false" to false
  3. Numeric coercion — if both sides can be parsed as numbers, compare as numbers. The string "42" coerces to 42, "1.5" to 1.5

ne is the inverse of eq — it uses the same coercion rules.

What passes

ActualExpectedResultWhy
42"42"passBoth parse as float64(42)
"1.0"1passBoth parse as float64(1)
true"true"passBoth coerce to boolean true
"FALSE"falsepassCase-insensitive boolean coercion
[1, 2][1, 2]passDeep equality

What fails (and may surprise you)

ActualExpectedResultWhy
"Hello""hello"faileq is case-sensitive for strings. Use eqIgnoreCase
" 42 "42failLeading/trailing whitespace prevents numeric parsing
null""failnull is not coerced to empty string. Use exists/notExists for null checks
"0"falsefailNumbers are not coerced to booleans — only "true"/"false" strings match booleans
0falsefailSame reason — no number-to-boolean bridge

ne inherits coercion. Because ne is !eq, coercion works in reverse: "42" ne 42 fails (they're considered equal). If you need to distinguish the string "42" from the number 42, use the type source field instead.

Numeric operators (gt, gte, lt, lte)

These always coerce both sides to numbers. If either side can't be parsed as a number, the assertion errors (not just fails). Numeric strings like "200" are coerced, but "fast" or true will error.

String operators (contains, matches)

Both sides are converted to their string representation before comparison. This means non-string values are stringified: numbers become "42", booleans become "true"/"false", objects become their Go fmt.Sprintf("%v") representation (which may not match JSON). For reliable results, use string operators on string values.

Tip: If you're unsure about a value's type, use the type source field to assert it explicitly before comparing: { "type": "$.response.body.count", "operator": "eq", "value": "number" }

Source fields

Source fields are alternatives to path that transform the resolved value before the operator is applied:

Source fieldResolves toDescription
typestringThe JavaScript type of the value at the given path: string, number, boolean, object, array, null
countnumberLength of an array or string at the given path
keysarrayArray of key strings from an object at the given path
valuesarrayArray of values from an object at the given path
entriesarrayArray of { key, value } objects from an object at the given path

Examples:

// Type check (replaces the old "type" operator)
{ "type": "$.response.body.items", "operator": "eq", "value": "array" }

// Length check (replaces the old "length" operator)
{ "count": "$.response.body.items", "operator": "eq", "value": 3 }

// Object keys
{ "keys": "$.response.body.config", "operator": "contains", "value": "timeout" }

ValueRef

Instead of a literal value, you can compare against another path in the document using { "from": "$.path" }:

{ "path": "$.response.body.createdAt", "operator": "eq", "value": { "from": "$.response.body.updatedAt" } }

This resolves both sides before comparing, enabling dynamic assertions that compare two fields against each other.