Skip to content

Test a response

A request can carry tests: assertions evaluated against its own response on every send. The same assertion shape is used by a chain’s assert node, and one evaluator in the engine serves both.

The Form view’s Tests tab edits them. In YAML:

- name: Create user
method: POST
url: "{{baseUrl}}/users"
body:
contentType: application/json
content: '{"email":"a@example.com"}'
tests:
- source: status
op: in
expected: "200, 201"
- source: body
path: id
op: exists
- source: header
path: content-type
op: contains
expected: application/json
- source: duration
op: lt
expected: "500"

An assertion is a source, an optional path, an op, and an optional expected.

sourceWhat path meansNotes
status— (unused)The numeric HTTP status
bodyA path into the body, e.g. data.items.0.idEvaluated by the same code that backs chain extraction — wildcards, .. and XPath included, see Path expressions
headerThe header name, e.g. content-typeMatched case-insensitively, so Content-Type works too
cookieThe cookie name, e.g. sessionRead from the shared jar, not just this response’s Set-Cookie — the cookie a login set three requests ago is usually the one you want
varA variable nameFrom the active environment for a request test, from chain scope in an assert node
duration— (unused)How long the send took, in milliseconds
size— (unused)The response body’s size in bytes

A source that is not there — a missing header, a path that addresses nothing — reports not present naming what it looked for, rather than silently comparing against an empty string.

Eighteen is too many to read as a flat list, so the editor groups them — Equality, Text, Numeric, Presence, Collection, Contract — and filters the list by source: a duration is never offered contains, a status is never offered startsWith. Changing a row’s source snaps its operator to one that can apply, instead of leaving a row that could never pass.

opMeaning
eqEquals expected
neDoes not equal expected
containsexpected occurs in the value
notContainsexpected does not occur — passes when the value is absent; nothing cannot contain something
startsWithThe value starts with expected
endsWithThe value ends with expected
matchesThe value matches the regular expression in expected
gt / gteNumerically greater than / at least expected
lt / lteNumerically less than / at most expected
betweenNumerically inside an inclusive range: 1..10 (or 1,10)
existsThe value is present and not empty — expected unused
isEmptyAbsent, "", [] or {} all count as empty — expected unused
lengthCounts array items, object keys, or characters; expected is a count like 3, >=1, <10 or 1..5
in / notInThe value is (not) one of 200, 201, 204 — [200, 201] is accepted too
schemaThe body matches the endpoint’s declared response schema — see below

There is deliberately no neq beside ne and no general negate flag: two on-disk spellings of one check is exactly what makes a committed format diff badly.

expected is a string, so quote a number in YAML (expected: "201"), and it may itself contain {{variables}}. Write a regular expression as the pattern alone (^application/(json|problem\+json)), not as a /…/flags literal. A numeric comparison against something that is not a number fails with a message naming which side was not a number, rather than just failing.

op: schema is the one assertion with nothing to type. It validates the response body against the response the bound OpenAPI spec declares for the status that actually came back:

tests:
- source: body
op: schema

Leave expected blank for that. Set it to a declared status (400, default) to check a different one — asserting that your error envelope matches the documented error shape, for example.

Four things worth knowing before you rely on it:

  • It only applies to source: body. Any other source reports that instead of quietly passing.
  • It needs a spec binding. With no declared response in scope it says so plainly — it never passes by default. That is also why the operator is offered on a request’s Tests tab and not in a chain’s assert node: a chain resolves its request nodes by id without carrying their endpoint bindings, so the option there would be dead.
  • A response the spec declares but gives no schema (a 204, say) holds — there is nothing to check.
  • For an array body only the first element is walked. That is enough to catch a reshaped item without an O(n) pass over a large collection.

Because the check reads the live spec rather than a pasted copy, it follows the API as it changes: nothing is snapshotted into your collection file.

The response pane gets a pass/fail strip — “4 passed”, or “1 of 4 failed” — and a Tests tab listing each assertion with what it expected and what it got. Run history stores results too, with a passed/total pill on the row, so you can see when an endpoint started failing, not just that it fails now.

SSE and other streaming sends do not evaluate tests. A request whose Accept is text/event-stream streams events rather than producing one buffered response, so there is nothing for the assertions to run against. The pane says which checks it skipped rather than staying quiet about it — silence there would be indistinguishable from everything passing. A streamed run is not a pass; it is unchecked.

An empty test list is dropped rather than written, so a request never gains tests: [] and your diffs stay minimal.

The assert node takes the same list, minus op: schema (above). All assertions passing routes to the pass branch; any failure routes to fail — which means a chain can check its own intermediate state and take a different path instead of just stopping — see Build a chain. The shared cookie jar rides along, so a cookie source works inside a chain exactly as it does for a request’s own tests.

- id: check-created
type: assert
position: { x: 400, y: 120 }
assertions:
- source: status
op: eq
expected: "201"
- source: duration
op: lt
expected: "1000"

Importing a Postman collection turns the recognised half of its pm.* scripts into real test rows rather than into more script: pm.response.to.have.status(200), .to.be.ok, .to.have.header(…) and the pm.expect chains become assertions on the request, and pm.environment.set(…) becomes an extraction — wired into a linear chain when a later request actually reads the value, and left out when nothing does. Anything it does not recognise is preserved as a commented-out hook and named in the import summary — a wrong translation would be a test that passes for the wrong reason, which is worse than no translation. See Import from curl & Postman.

Seven sources and eighteen operators is the whole vocabulary at v0.1.0-preview.10 — it grew from four and seven in the previous release, and the earlier files still load unchanged. What is still not here: op: schema inside a chain’s assert node, and any evaluation at all on a streaming send.