Skip to main content

Module assertions

Module assertions 

Source
Expand description

Declarative checks a request makes against the response it gets back.

A request file may carry an assertions block:

method: GET
url: https://httpbin.org/get
assertions:
  status: 200
  status_in: [200, 201, 204]
  headers:
    content-type: application/json   # present, with this exact value
    x-request-id:                    # present, value not checked
  body_contains: '"url"'
  body_matches: '^\{"url"'
  elapsed_ms_under: 2000
  json:
    $.headers.Accept: application/json           # bare value: equality
    $.count: { greater_than: 5 }                 # operator object: comparison
    $.id: { matches: '^[0-9a-f-]{36}$' }         # operator object: regex
  not:
    status: 404

Every key is optional and a request with no assertions block behaves exactly as it did before this module existed.

Two things need a word up front, because they are not obvious from the schema alone:

json: disambiguates bare values from operators by shape. A path’s expected value is read as an equality check unless it is a YAML mapping with exactly one key drawn from greater_than, greater_than_or_equal, less_than, less_than_or_equal, contains, length or matches, in which case it is that operator instead. This means an equality check against a genuine one-key object shaped like {greater_than: 5} is not expressible — a real limitation, accepted because the alternative (a separate block for operators) would make every path assertion say twice which kind it is, and {greater_than: 5} as a literal expected value is not a shape real APIs return. A multi-key object ({id: 1, name: ada}) is never ambiguous and is always equality, exactly as before.

There is deliberately no not_equal operator: not: {json: {$.count: 5}} already says “not equal to 5” precisely, and a dedicated operator would only be a shorter spelling of the negation wrapper that already exists — unlike greater_than/less_than, which are comparisons not: cannot express at all (not: {json: {$.count: {greater_than: 5}}} means <= 5, not < 5, and there is no operator-free way to write “less than 5” as a negation).

not: wraps a whole assertions block, not one assertion. It takes the same keys as the top level (minus not itself — nesting not inside not is a parse error, not a double negative) and negates each one independently: not: {status: 404, body_contains: error} is “status is not 404” and “body does not contain error”, not “status is 404 and body contains error” negated as a pair. A wrapper was chosen over a not_status / not_body_contains key for every assertion type because it composes for free with whatever assertion kind is added next, rather than doubling the schema’s key count every time one is.

A hard error — a malformed JSON path, an invalid regex (whether from body_matches or a matches operator), a body that is not JSON, a JSON path selecting zero or several values, a comparison or length operator applied to a value of the wrong type — is not something not: can turn into a pass. These are facts about the request or the file, not a condition to be true or false, so not: {json: {$.a: {greater_than: 5}}} against a body where $.a is a string still fails, the same way it would unwrapped.

Evaluation never fails. Assertions::evaluate returns an AssertionReport and no Result: everything that could go wrong — a body that is not JSON, a JSON path that does not parse, a header that is not there — is a failed assertion with a message, not an error in the surrounding run. The response has already arrived by the time any of this happens, so there is nothing left to abort; the only useful thing to do with a broken expectation is to say precisely how it broke, next to the ones that held.

Nothing here decides an exit code. Evaluating and reporting is all this module does; whether a failed assertion should fail the process is a front-end decision, and today the answer is no. See the exit-code table in sendra-cli.

Structs§

AssertionReport
Every assertion on one request, evaluated against one response, in a fixed order: status, status_in, headers, body_contains, body_matches, elapsed_ms_under, then JSON paths — with the entries of each map in sorted order — followed by the same order again for the not: block, if there is one. Deterministic because the output is read by people and diffed by scripts, and neither is served by an order that depends on how a BTreeMap happened to be filled.
AssertionResult
One assertion, evaluated.
Assertions
The assertions block of a request, exactly as it appears on disk.
NotAssertions
The inner block of a not: wrapper: every key Assertions has, except not itself.

Enums§

AssertionKind
Which kind of check produced a result, for a front-end that wants to group, filter or colour by kind rather than parse the rendered text.