Skip to content

Assertions: .expect() and .check()

.expect() and .check() validate response properties using scopes --- named checks against parts of the response like status code, timing, body, and headers.

The only difference between them: .expect() failures are hard, .check() failures are soft (recorded, execution continues). A hard fail skips the call's .store() and .wait() and every later call; the same call's .check() and .assert() still run and are recorded. See Failure Semantics.

Complete Evaluation

Both methods evaluate all their scopes before any failure takes effect. If .expect() has three scopes and two fail, both failures are recorded together --- you see every problem at once, not just the first.

// If both status and timing fail, you see both failures in the result
.expect(
  status: 200,
  totalDelayMs: 500
)

Scope Forms

Every scope accepts three forms.

Shorthand --- value only

The operator is inferred from the scope name (see Default Operators):

.expect(
  status: 200,           // eq (exact match)
  totalDelayMs: 500      // lt (less than)
)

For status, you can pass an array --- any match passes:

.expect(status: [200, 201, 204])

Value + operator

Override the default operator:

.expect(
  totalDelayMs: { value: 500, op: "lte" }   // less than or equal
)

Full form --- with options

Includes the options block, which is a placeholder for extensions:

.expect(
  status: {
    value: 200,
    op: "eq",
    options: {
      // Extension fields go here (e.g. notification templates)
    }
  }
)

All three forms produce the same core behaviour. The full form exists so extensions can attach metadata to individual scopes.

Available Scopes

Scope Type Description
status integer or integer[] HTTP status code. Array means "any of these".
body body match Body content or schema validation. See Body Matching.
headers object Each key-value pair must match. Keys are case-insensitive.
bodySize size string Body size threshold (e.g. "50kb", "10m"). Also gates body capture: a body over the limit is not saved and the call records bodyNotCapturedReason: "bodyTooLarge".
totalDelayMs integer Total response time threshold in ms.
dns integer DNS resolution time threshold in ms.
connect integer TCP connection time threshold in ms.
tls integer TLS handshake time threshold in ms. Skipped when this.tlsMs is 0 (plain HTTP).
ttfb integer Time to first byte threshold in ms.
transfer integer Body transfer time threshold in ms.
size integer Exact response body size in bytes.
redirects string URL match against the redirect hops (this.redirects). See Redirects Scope.

Size string format for bodySize: a number optionally followed by a unit --- 50, 50k, 50kb, 10m, 10mb, 1g, 1gb.

Default Operators

When you omit op, each scope uses a natural default:

Scope Default op Why
status eq Status codes are exact matches
body eq Exact or schema match
headers eq Each header matched exactly
size eq Exact byte count
redirects eq URL equality
totalDelayMs lt "Must be faster than this"
dns lt Timing threshold
connect lt Timing threshold
tls lt Timing threshold
ttfb lt Timing threshold
transfer lt Timing threshold
bodySize lt Size threshold

The available operators are: lt, lte, eq, neq, gte, gt.

Body Matching

The body scope supports three kinds of matching:

Schema validation

Validates the response JSON against a JSON Schema stored in a script variable:

.expect(body: schema($user_schema))

The variable must contain a valid JSON Schema document. If the variable is null, this is a hard fail.

Schema match modes control how extra fields are handled:

// Loose (default): schema fields must be present and valid, extra fields allowed
get("$BASE_URL/api/user")
.expect(body: schema($user_schema))

// The same, with the mode written out
get("$BASE_URL/api/user")
.expect(body: { value: schema($user_schema), mode: "loose" })

// Strict: no extra fields allowed at any level
get("$BASE_URL/api/user")
.expect(body: { value: schema($user_schema), mode: "strict" })
Mode Behaviour
loose (default) All schema fields must be present and valid. Additional fields are ignored.
strict All schema fields must be present and valid. Any extra field at any level fails.

When a schema check fails, the assertion record's actual carries the first validation error path and message (e.g. { path: ".user.id", detail: "expected integer, got string" }). A strict-mode extra field is reported with path .<field> and detail "unexpected field".

Literal string

The raw response body must match the string exactly:

.expect(body: "OK")

Variable match

The raw response body must equal the runtime value of the variable:

.expect(body: $expected_body)

Note

The mode field only applies to schema(...) body matching. Literal string and variable matches are always byte-exact; mode on those is silently ignored.

Redirects Scope

The redirects scope asserts against the redirect hops followed during the call (this.redirects, see Redirect Tracking). It uses a match field to select which hop to compare:

// Shorthand --- defaults to match: "any"
get("$BASE_URL/account")
.expect(redirects: "/login")

// Full form --- explicit match type
get("$BASE_URL/account")
.expect(redirects: { value: "/login", match: "first" })

get("$BASE_URL/old-home")
.check(redirects: { value: "/final", match: "last" })
match Behaviour
first Compare against the first redirect hop. Fails if no redirects occurred.
last Compare against the last redirect hop. Fails if no redirects occurred.
any (default) Pass if the value matches any redirect in the chain.

eq is the only operator redirects accepts --- ordered comparisons are not meaningful for URLs.

Combining .expect() and .check()

A common pattern: use .expect() for must-not-break thresholds and .check() for performance goals:

get("$BASE_URL/api/search")
.expect(
  status: 200,
  totalDelayMs: 10000     // hard fail: over 10 seconds is broken
)
.check(
  totalDelayMs: 1000,     // soft fail: over 1 second is slow
  dns: 50,
  tls: 100
)