Testing APIs
Start with declarative assertions for simple checks. Use tests when the check
needs conditions, loops, or comparisons between values. Copy a request into your
collection and adapt api.example.com and the response fields to your API.
Each recipe states the response shape it expects.
Tests run after captures, post scripts, and assertions whenever a response exists. Inherited tests run collection → outermost folder → nearest folder → request. They can read the final prepared request, response, and RunScope, but cannot mutate state or make HTTP calls. See the matcher reference for supported checks.
Check a basic response contract
Section titled “Check a basic response contract”Use assertions when an endpoint must return HTTP 200, JSON, and a response in
under one second. This example expects a JSON body with status: "ok":
name: Check service healthmethod: GETurl: https://api.example.com/healthassert: - expression: status operator: equals value: 200 - expression: headers.Content-Type operator: contains value: application/json - expression: body.status operator: equals value: ok - expression: response.time operator: lt value: 1000All four assertions must pass. Choose the timing budget for your environment; network latency contributes to response time. No script is needed for these checks.
Validate a JSON schema
Section titled “Validate a JSON schema”Use a schema when several fields must keep their types. This endpoint should
return an object such as {"id":7,"email":"reader@example.com","active":true}:
name: Validate a user responsemethod: GETurl: https://api.example.com/users/7tests: |- test("user matches the response contract", () => { expect(noodle.response.status).toBe(200); expect(noodle.response.json()).toMatchSchema({ type: "object", required: ["id", "email", "active"], properties: { id: { type: "integer", minimum: 1 }, email: { type: "string", format: "email" }, active: { type: "boolean" }, }, }); });The test fails for missing required fields, invalid email formats, and incorrect
types. Additional fields are allowed. Schema validation does not coerce "7"
into the integer 7; see JSON Schema support.
Keep sensitive fields out of responses
Section titled “Keep sensitive fields out of responses”Use this for a public user endpoint whose JSON body is an object. The response
may contain fields such as id and name, but must omit credential fields:
name: Check the public user representationmethod: GETurl: https://api.example.com/users/7tests: |- test("public user omits credential fields", () => { expect(noodle.response.status).toBe(200); const user = noodle.response.json(); expect(user).toMatchSchema({ type: "object" }); for (const field of ["password", "password_hash", "access_token", "refresh_token"]) { expect(user).not.toHaveProperty(field); } });An omitted field passes; a field present with null or an empty string fails.
This checks only the named top-level fields. Apply the same checks to nested
objects if your response contains them, and adapt the names to your contract.
This recipe does not inspect every possible location or spelling of a secret.
Check an empty 204 response
Section titled “Check an empty 204 response”Use this for an endpoint whose successful response is HTTP 204 with no body. This example assumes a readiness endpoint with that contract:
name: Check readiness without a response bodymethod: GETurl: https://api.example.com/health/readytests: |- test("readiness returns 204 and an empty body", () => { expect(noodle.response.status).toBe(204); expect(noodle.response.text()).toBe(""); });Read the body as text. Calling json() on an empty body would fail. A 200
response fails this contract even if its body is empty. Avoid inheriting a
shared test that requires JSON from this endpoint.
Check every item in an array
Section titled “Check every item in an array”Use a loop when all returned items must satisfy a condition. This endpoint should return a non-empty array of active users, each with a positive numeric ID:
name: Check active usersmethod: GETurl: https://api.example.com/users?active=truetests: |- test("every returned user is active", () => { expect(noodle.response.status).toBe(200); const users = noodle.response.json(); expect(Array.isArray(users)).toBe(true); expect(users.length).toBeGreaterThan(0); for (const user of users) { expect(user.id).toBeGreaterThan(0); expect(user.active).toBe(true); } });The non-empty check prevents an empty response from passing without checking any users. Remove that check only if an empty result is valid for this endpoint. A failed matcher ends this test; other declared tests still run.
Compare related fields
Section titled “Compare related fields”Use JavaScript when a response contains values that must agree. This example
expects integer minor-unit amounts, such as
{"subtotal":1000,"tax":200,"total":1200}:
name: Check order totalsmethod: GETurl: https://api.example.com/orders/demo-ordertests: |- test("total equals subtotal plus tax", () => { expect(noodle.response.status).toBe(200); const order = noodle.response.json(); for (const amount of [order.subtotal, order.tax, order.total]) { expect(Number.isSafeInteger(amount)).toBe(true); expect(amount).toBeGreaterThanOrEqual(0); } expect(Number.isSafeInteger(order.subtotal + order.tax)).toBe(true); expect(order.total).toBe(order.subtotal + order.tax); });Matching totals pass; a mismatch or non-integer amount fails. Adapt the formula if your API includes shipping, discounts, or other components.
Verify sorting and pagination
Section titled “Verify sorting and pagination”Use related checks for a paginated list. This endpoint should return
{"page":2,"items":[{"price":10},{"price":20}]}, with at most two items in
ascending price order:
name: Check a sorted product pagemethod: GETurl: https://api.example.com/productsparams: - name: page value: "2" - name: limit value: "2" - name: sort value: pricetests: |- test("response matches the requested page", () => { expect(noodle.response.status).toBe(200); const page = noodle.response.json(); expect(page.page).toBe(2); expect(Array.isArray(page.items)).toBe(true); expect(page.items.length).toBeLessThanOrEqual(2); }); test("prices are in ascending order", () => { const items = noodle.response.json().items; expect(Array.isArray(items)).toBe(true); for (let index = 0; index < items.length; index++) { expect(Number.isFinite(items[index].price)).toBe(true); if (index > 0) { expect(items[index].price).toBeGreaterThanOrEqual(items[index - 1].price); } } });An empty page is allowed here. Equal prices pass. These checks validate the returned page; they do not fetch other pages or prove that the full dataset is complete. Use request chaining to fetch related resources before checking them.
Check that the response reflects the request
Section titled “Check that the response reflects the request”Use this when an update should retain the fields you sent. The endpoint should
return HTTP 200 and a user object containing name and active; additional
fields such as id are allowed:
name: Verify an updated usermethod: PATCHurl: https://api.example.com/users/7body_type: jsonbody: '{"name":"Updated Cookbook User","active":false}'tests: |- test("saved fields match the submitted values", () => { expect(noodle.response.status).toBe(200); const sent = noodle.request.body.json(); expect(noodle.response.json()).toMatchObject({ name: sent.name, active: sent.active, }); });This reads the final prepared body, including substitution and pre-script
changes. A different name, a missing field, or active: true fails. Compare
only fields your API promises to preserve; adapt the expectation for fields
the server intentionally normalizes.
Detect duplicate IDs
Section titled “Detect duplicate IDs”Use this to catch repeated records in a list response. This example expects
an array of objects with positive integer IDs, such as [{"id":7},{"id":8}]:
name: Check user IDs are uniquemethod: GETurl: https://api.example.com/userstests: |- test("user IDs are valid and unique", () => { expect(noodle.response.status).toBe(200); const users = noodle.response.json(); expect(Array.isArray(users)).toBe(true); const ids = users.map(user => user.id); for (const id of ids) { expect(Number.isSafeInteger(id)).toBe(true); expect(id).toBeGreaterThan(0); } expect(new Set(ids).size).toBe(ids.length); });Duplicate IDs, missing IDs, and string IDs fail. An empty array passes because it contains no duplicates; add a length check if this endpoint must return records. This checks one response, not uniqueness across other pages.
Report one test per record
Section titled “Report one test per record”Use separate named tests when one invalid item should not hide failures in
later items. This endpoint should return a non-empty array of active users
with positive integer IDs, such as [{"id":7,"active":true},{"id":8,"active":true}]:
name: Report each active user separatelymethod: GETurl: https://api.example.com/users?active=truetests: |- const users = noodle.response.json(); test("response is a non-empty user list", () => { expect(noodle.response.status).toBe(200); expect(Array.isArray(users)).toBe(true); expect(users.length).toBeGreaterThan(0); }); if (Array.isArray(users)) { for (const [index, user] of users.entries()) { test(`user ${user?.id ?? "missing ID"} at row ${index + 1} is active`, () => { expect(user).toMatchObject({ active: true }); expect(Number.isSafeInteger(user.id)).toBe(true); expect(user.id).toBeGreaterThan(0); }); } }Results shows one list check plus a row for every user. An inactive user fails its own test while later users are still checked. Row numbers distinguish duplicate or missing IDs. Empty arrays fail the list check instead of passing with zero item tests. For large lists, use a small page to keep Results readable and stay within script result limits.
Validate timestamp order and freshness
Section titled “Validate timestamp order and freshness”Use this for a record that should have been updated recently, such as the
latest heartbeat. The response must contain created_at and updated_at as
ISO timestamps with Z or an explicit timezone offset:
name: Check the latest heartbeat timestampsmethod: GETurl: https://api.example.com/heartbeats/latesttests: |- test("timestamps are valid, ordered, and recent", () => { expect(noodle.response.status).toBe(200); const heartbeat = noodle.response.json(); expect(heartbeat.created_at).toMatch(/T/); expect(heartbeat.updated_at).toMatch(/T/); const created = noodle.time.parse(heartbeat.created_at); const updated = noodle.time.parse(heartbeat.updated_at); const now = noodle.time.now(); expect(created).toBeLessThanOrEqual(updated); expect(updated).toBeGreaterThanOrEqual(now - 15 * 60 * 1000); expect(updated).toBeLessThanOrEqual(now + 30 * 1000); });Invalid calendar dates and timestamps without a timezone fail parsing. The
T checks also reject date-only strings. Creation must precede or equal the
update; the update must be within the last 15 minutes, with up to 30 seconds of
future clock skew. Adjust both windows to your API’s contract and keep the
client clock synchronized. This freshness rule is unsuitable for intentionally
old records. See the time API.
Use expected values from a dataset
Section titled “Use expected values from a dataset”Use one request to check several known records against different expectations.
Save this JSON as data/users.json, relative to the directory where you will
run the CLI:
[ { "user_id": 7, "expected_active": true, "expected_role": "member" }, { "user_id": 8, "expected_active": false, "expected_role": "admin" }]Save the request as users/check.yml inside your api collection. The endpoint
should return id, active, and role for the requested user:
name: Check a user against dataset expectationsmethod: GETurl: https://api.example.com/users/$user_idtests: |- test("user matches the dataset row", () => { expect(noodle.iteration).not.toBeNull(); expect(noodle.response.status).toBe(200); const expected = noodle.iteration.data; expect(noodle.response.json()).toMatchObject({ id: expected.user_id, active: expected.expected_active, role: expected.expected_role, }); });Run from the directory containing api/ and data/:
noodle collection run ./api users/check --data ./data/users.jsonThe request runs twice, with a separate result for each row. Both pass only if
the API matches the respective expectations. noodle.iteration.data keeps the
original row even if a capture or script overwrites a RunScope value with the
same name. Each row starts with a fresh scope.
JSON preserves numbers and booleans. CSV cells are strings, so a CSV version
needs explicit conversions before typed comparisons. Running without a dataset
leaves noodle.iteration null; ordinary substitution also needs a value for
user_id. See iteration data
for the F5 Runner and path rules.
Share checks across a folder
Section titled “Share checks across a folder”Use inherited tests when every request in a folder shares a contract. Merge
this block into users/folder.yml if all its endpoints return JSON on success:
tests: |- test("successful users responses contain JSON", () => { if (noodle.response.status < 400) { expect(noodle.response.headers.get("Content-Type")).toContain("application/json"); expect(noodle.response.json()).toBeDefined(); } });Collection tests run first, then ancestor folder tests, then request tests.
Every block gets a fresh sandbox and runs once per request, including each
dataset row. Add this only to folders with the stated contract; an empty 204
response would fail its JSON check. Use collection settings.yml instead
when the rule applies to the entire collection. For reuse across selected
scopes, put the source in an external JavaScript file.
Read failures and run the checks
Section titled “Read failures and run the checks”Save the first recipe as health.yml and run it manually, in the F5 Runner, or
from the CLI:
noodle request run health --collection ./apiInspect Response → Results for assertion and test outcomes. Failed assertions, failed tests, or a top-level test error make automation fail. HTTP 400 or higher also fails the request, even if a conditional test returns early or a test expecting that status passes. Pre and transport failures leave tests unevaluated. See CLI automation and CI for collection runs and JSON results.