1) Essentials of the Tests tab and Postman’s assertion style
Postman test scripts run after a request completes. Their job is to turn a raw response into automated checks that answer: “Did the API behave correctly?” Reliable validation focuses on behavior (contracts, invariants, and error handling), not just “it returned something.”
In the Tests tab, you write JavaScript that uses Postman’s built-in pm API plus the Chai assertion style via pm.expect. Tests are grouped with pm.test(name, fn). A good test name reads like a requirement and fails with a message that helps you diagnose quickly.
pm.test("returns JSON", function () { pm.response.to.have.header("Content-Type"); pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");});Key objects you will use constantly:
pm.response: status, headers, response time, and body parsing helpers.pm.response.code: numeric HTTP status code.pm.response.responseTime: milliseconds for the request.pm.response.json(): parse JSON body (throws if invalid JSON).pm.expect(value): assertions (e.g.,.to.equal,.to.be.a,.to.have.property).
Practical mindset: prefer checks that stay stable when non-essential data changes. For example, validate that an id is a non-empty string, not that it equals a specific value (unless the test is explicitly about that value).
2) Assertions for status codes, response time thresholds, headers, and schema-like checks
Status code assertions
Status codes are the first contract. Use explicit expectations for the scenario you are testing.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
pm.test("status is 200", function () { pm.expect(pm.response.code, "HTTP status").to.equal(200);});If multiple success codes are acceptable (for example, 200 or 304), assert a set membership.
pm.test("status is a success code", function () { pm.expect(pm.response.code, "HTTP status").to.be.oneOf([200, 304]);});Response time thresholds
Response time checks should be realistic and environment-aware. Use thresholds to catch regressions, not to enforce an unrealistically tight number.
pm.test("response time under 800ms", function () { pm.expect(pm.response.responseTime, "response time (ms)").to.be.below(800);});Header assertions
Headers validate content negotiation, caching, and security-related behavior. Focus on what your API guarantees.
pm.test("Content-Type is JSON", function () { const ct = pm.response.headers.get("Content-Type"); pm.expect(ct, "Content-Type").to.include("application/json");});Example: ensure a request is not cached unexpectedly (only if your API contract specifies it).
pm.test("Cache-Control prevents caching", function () { const cc = pm.response.headers.get("Cache-Control"); pm.expect(cc, "Cache-Control").to.match(/no-store|no-cache/i);});Schema-like checks (without a full schema validator)
Even without a JSON Schema library, you can do “schema-like” validation by asserting types, required keys, and array shapes. This catches breaking changes while staying readable.
pm.test("body has required top-level fields", function () { const body = pm.response.json(); pm.expect(body, "body").to.be.an("object"); pm.expect(body).to.have.property("id"); pm.expect(body).to.have.property("name");});For arrays, validate that it is an array and that items have expected structure.
pm.test("items is an array of objects with id", function () { const body = pm.response.json(); pm.expect(body.items, "items").to.be.an("array"); if (body.items.length > 0) { pm.expect(body.items[0], "first item").to.be.an("object"); pm.expect(body.items[0]).to.have.property("id"); }});3) Validating JSON structure and key fields
Reliable validation typically combines: (1) parseability, (2) required structure, (3) key field constraints, and (4) cross-field rules. The goal is to confirm the API’s behavior rather than overfitting to a single response instance.
Step-by-step: build a test suite for one endpoint
Assume an endpoint GET /users/:id returns a user object. The exact URL is not important; the pattern is. You will build the suite incrementally in the request’s Tests tab.
Step 1: basic contract (status + JSON)
pm.test("status is 200", function () { pm.expect(pm.response.code, "HTTP status").to.equal(200);});pm.test("response is JSON", function () { const ct = pm.response.headers.get("Content-Type"); pm.expect(ct, "Content-Type").to.include("application/json"); pm.expect(function () { pm.response.json(); }, "valid JSON").to.not.throw();});Step 2: top-level structure and required keys
pm.test("body has required user fields", function () { const user = pm.response.json(); pm.expect(user, "user").to.be.an("object"); pm.expect(user).to.have.property("id"); pm.expect(user).to.have.property("email"); pm.expect(user).to.have.property("createdAt");});Step 3: type and format checks for key fields
Type checks catch contract drift (e.g., id changing from string to number). Format checks should be minimal and robust.
pm.test("user field types are correct", function () { const user = pm.response.json(); pm.expect(user.id, "id").to.be.a("string").and.to.have.length.greaterThan(0); pm.expect(user.email, "email").to.be.a("string"); pm.expect(user.createdAt, "createdAt").to.be.a("string");});pm.test("email looks like an email", function () { const user = pm.response.json(); pm.expect(user.email, "email format").to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);});pm.test("createdAt is ISO-like", function () { const user = pm.response.json(); pm.expect(user.createdAt, "createdAt format").to.match(/^\d{4}-\d{2}-\d{2}T/);});Step 4: cross-field and behavioral checks
Behavioral checks validate rules, not just presence. Examples: a flag implies a field exists; a role controls permissions; an “active” user must not have a “deactivatedAt” timestamp.
pm.test("active users do not have deactivatedAt", function () { const user = pm.response.json(); if (user.active === true) { pm.expect(user.deactivatedAt, "deactivatedAt when active").to.be.oneOf([null, undefined]); }});Step 5: performance and header behavior
pm.test("response time under 800ms", function () { pm.expect(pm.response.responseTime, "response time (ms)").to.be.below(800);});pm.test("Content-Type is JSON", function () { const ct = pm.response.headers.get("Content-Type"); pm.expect(ct, "Content-Type").to.include("application/json");});Step 6: optional fields and arrays (validate shape when present)
Optional fields should be validated conditionally: if present, they must be correct.
pm.test("roles is an array of strings when present", function () { const user = pm.response.json(); if (user.roles !== undefined && user.roles !== null) { pm.expect(user.roles, "roles").to.be.an("array"); user.roles.forEach(function (r, i) { pm.expect(r, "roles[" + i + "]").to.be.a("string").and.to.have.length.greaterThan(0); }); }});4) Negative testing (expected 4xx/5xx) and error-body validation
Negative tests verify that the API fails correctly: correct status code, consistent error shape, and useful messages or codes. This is essential for reliable clients and for preventing accidental “success” responses on invalid input.
Example: invalid ID returns 404 with a structured error
Create a request that calls GET /users/:id with a non-existent ID. Your checks should validate both the status and the error body contract.
pm.test("status is 404", function () { pm.expect(pm.response.code, "HTTP status").to.equal(404);});pm.test("error response is JSON", function () { const ct = pm.response.headers.get("Content-Type"); pm.expect(ct, "Content-Type").to.include("application/json");});pm.test("error body has code and message", function () { const err = pm.response.json(); pm.expect(err, "error body").to.be.an("object"); pm.expect(err).to.have.property("error"); pm.expect(err.error).to.have.property("code"); pm.expect(err.error).to.have.property("message"); pm.expect(err.error.code, "error.code").to.be.a("string"); pm.expect(err.error.message, "error.message").to.be.a("string");});Example: validation error returns 400 with field-level details
For endpoints that validate input, assert that the API returns actionable details. Even if your API uses a different shape, the pattern is the same: check that the response includes machine-readable identifiers and human-readable messages.
pm.test("status is 400", function () { pm.expect(pm.response.code, "HTTP status").to.equal(400);});pm.test("validation errors include fields", function () { const err = pm.response.json(); pm.expect(err).to.have.property("errors"); pm.expect(err.errors, "errors").to.be.an("array"); if (err.errors.length > 0) { pm.expect(err.errors[0]).to.have.property("field"); pm.expect(err.errors[0]).to.have.property("message"); }});Expected 5xx: treat as a contract too
In many teams, 5xx responses are not “expected” for functional tests, but they can be expected in resilience or chaos scenarios. If you do test them, validate that the API returns a safe, consistent error body (no stack traces) and a correlation identifier if your contract includes one.
pm.test("status is 503", function () { pm.expect(pm.response.code, "HTTP status").to.equal(503);});pm.test("error body does not expose internals", function () { const text = pm.response.text(); pm.expect(text, "no stack trace").to.not.match(/Exception|StackTrace|at\s+\w+\./i);});5) Test readability with helper functions and consistent messages
As your suite grows, readability and consistency become more important than cleverness. Use helper functions to reduce duplication, standardize failure messages, and make copy-and-adapt safe.
Helper functions pattern
Define small helpers at the top of the Tests script. Keep them pure and focused.
function expectStatus(expected) { pm.test("status is " + expected, function () { pm.expect(pm.response.code, "HTTP status").to.equal(expected); });}function getJson() { pm.test("response is valid JSON", function () { pm.expect(function () { pm.response.json(); }, "valid JSON").to.not.throw(); }); return pm.response.json();}function expectJsonContentType() { pm.test("Content-Type is JSON", function () { const ct = pm.response.headers.get("Content-Type"); pm.expect(ct, "Content-Type").to.include("application/json"); });}function expectResponseTimeUnder(ms) { pm.test("response time under " + ms + "ms", function () { pm.expect(pm.response.responseTime, "response time (ms)").to.be.below(ms); });}Then your tests read like a checklist:
expectStatus(200);expectJsonContentType();expectResponseTimeUnder(800);const user = getJson();pm.test("user has id, email, createdAt", function () { pm.expect(user).to.have.property("id"); pm.expect(user).to.have.property("email"); pm.expect(user).to.have.property("createdAt");});Consistent messages and naming
Guidelines for stable, debuggable tests:
- Use requirement-style names:
"user.email is a string"instead of"email test". - Include the field name in the assertion message:
pm.expect(user.email, "user.email").... - Prefer one responsibility per test block. If a test fails, you should know what broke.
- Validate optional fields conditionally to avoid false failures.
Replicate patterns across endpoints (copy-and-adapt guidelines)
Once one endpoint has a clean suite, replicate the pattern across other requests by copying the helper block and adapting only the endpoint-specific checks.
Copy-and-adapt checklist:
- Keep shared helpers identical across requests (status, JSON content-type, response time, JSON parsing).
- Change only the contract section: required fields, types, and cross-field rules for that endpoint.
- Align thresholds per endpoint category (e.g., search endpoints may have higher response time budgets than simple reads).
- Standardize error validation: for each endpoint, add at least one negative test that validates status + error shape.
- Avoid hardcoding volatile values: do not assert exact timestamps, generated IDs, or array ordering unless the contract guarantees them.
Example: adapting the same suite to a list endpoint
For GET /users, reuse the same helpers, then validate that the response is an array (or an object with an items array) and that each item has the minimal user shape.
expectStatus(200);expectJsonContentType();expectResponseTimeUnder(1200);const body = getJson();pm.test("returns an array of users", function () { pm.expect(body, "body").to.be.an("array"); body.forEach(function (u, i) { pm.expect(u, "user[" + i + "]").to.be.an("object"); pm.expect(u.id, "user[" + i + "].id").to.be.a("string"); pm.expect(u.email, "user[" + i + "].email").to.be.a("string"); });});This approach scales: every request gets the same baseline checks (status, JSON, time, headers) plus a small, focused set of contract checks and at least one negative test. The result is a suite that detects breaking changes and validates behavior reliably.