Scripts & tests

Two ways to check a response: declarative assertions stored as data, and JavaScript that runs in a sandbox with a Postman-compatible pm.* surface. The app authors tests as scripts; the declarative form is still read and run by the engine.

Declarative assertions

An assertion is a row in the request file. There is no code to review, no runtime to trust, and the failure message writes itself:

[[tests]]
kind = "status"
op = "eq"
value = 200

[[tests]]
kind = "json"
path = "$.data.items"
op = "len"
value = 25

[[tests]]
kind = "header"
name = "content-type"
op = "contains"
value = "application/json"

[[tests]]
kind = "duration"
op = "lt"
value = 500

Four kinds exist, and these are all the operators each one takes:

Assertion kinds and their operators
KindFieldsOperators
statusvalue (number)eq, ne, lt, gt
jsonpath (JSONPath), value (optional)eq, ne, exists, absent, contains, matches, lt, gt, len
headername, value (optional)exists, absent, eq, contains, matches
durationvalue (milliseconds)lt, gt

Notes that save you a debugging session: path is a real JSONPath rooted at $; matches takes a regular expression; len asserts on the length of a string, array or object; header names are matched case-insensitively; and numeric comparison is numeric, so 1 and 1.0 compare equal. A json assertion against a body that is not JSON fails with that reason rather than pretending the path was missing.

Scripts

Each request can carry a pre-request script and a post-response script. They run in a QuickJS engine embedded in the Rust core — not in Node, not in the browser, not in a hidden window.

Order of execution for one request: pre-request script → variable resolution → send → declarative assertions and captures → post-response script.

The pm.* surface

This is the complete list. Anything not here does not exist:

  • pm.inforequestName, eventName, iteration, iterationCount.
  • pm.environment, pm.variables, pm.globals, pm.collectionVariables — the same variable bag under four names, with get, set, unset, has, clear, toObject and replaceIn. Mándalo has one scope, not four; the aliases exist so pasted Postman scripts keep working.
  • pm.requestmethod, url, body, all writable from a pre-request script, plus pm.request.headers with add, upsert, remove, has, get, all, each and count. Headers are { key, value } objects. Edits made before the send are applied to the outgoing request.
  • pm.response — only in a post-response script. code, status, responseTime, responseSize, text(), json(), and headers with get, has and all.
  • pm.response.toto.have.status(200) or status("OK"), to.have.header(name), to.have.body(), to.have.jsonBody(), to.be.ok, to.be.success, to.be.clientError, to.be.serverError, and to.not.… for all of them.
  • pm.test(name, fn) — runs fn, records a pass or a failure with the thrown message. A failing test never aborts the run.
  • pm.expect(value) — a Chai-shaped assertion chain: equal/eql, a/an, above/below/least/most, include/contain, property, lengthOf, match, oneOf, the true/false/null/undefined/ok/empty flags, and not to negate.
  • console.log, info, debug, warn, error — collected and shown with the response.
// post-response
pm.test("charge was created", function () {
  pm.response.to.have.status(201);
  const body = pm.response.json();
  pm.expect(body.currency).to.equal("eur");
  pm.expect(body.id).to.be.a("string");
});

pm.environment.set("chargeId", pm.response.json().id);
console.log("captured", pm.environment.get("chargeId"));

What we deliberately don't support

The sandbox is small on purpose, and every gap throws a message telling you what is missing and why rather than failing as undefined:

  • No Node APIs and no modules. require, module, exports and process are absent. There is no module loader, so there is no supply chain to audit inside your API client.
  • No filesystem. A script cannot read or write files. Your collections cannot be modified by a script that came in with an import.
  • No network from scripts. fetch, XMLHttpRequest, WebSocket and pm.sendRequest all refuse. A request should send one request; if you need a chain, chain requests and use captures.
  • No timers and no async. setTimeout, setInterval, setImmediate and queueMicrotask are absent — scripts are synchronous, so a run finishes deterministically.
  • No browser or host globals. window, document, localStorage, Deno and Bun are absent.
  • Bounded execution. Each script gets a memory ceiling and a wall-clock timeout (32 MB and 2 seconds by default). An infinite loop is interrupted; it does not hang the app.
  • Not implemented yet, and they say so: pm.execution, pm.cookies, pm.visualizer, pm.iterationData, pm.vault and pm.setNextRequest each throw an explanatory error when touched.
Practical consequence

Postman scripts that only assert on the response and move variables around usually run unchanged. Scripts that fetch a second URL, use a cookie jar, or drive the collection runner need rewriting. The import brings scripts across verbatim and flags them so you can find out at review time rather than at run time.

Which one should you use

Use a declarative assertion whenever it expresses the check. It is data, so it reviews as data, it cannot loop forever, and the assertion name in the result is generated from the rule itself. Drop into a script when you need a computed expectation, a value derived from several fields, or a shape check that no single operator covers.