Resource library

QA How-To

Postman Test Scripts: pm.expect Examples for API Testing

Postman test scripts pm.expect examples for status, headers, JSON, arrays, errors, and chained requests, with a runnable local API and verification steps.

18 min read | 2,897 words

TL;DR

Put JavaScript in a request's Scripts > Post-response tab. Wrap each behavior in a named pm.test, then use pm.expect against pm.response.code, headers, JSON fields, or collection variables. Run the saved requests locally to verify both happy paths and expected errors.

Key Takeaways

  • Use named pm.test blocks so the failing API behavior is visible in Test Results.
  • Check status, media type, and response fields separately for useful failures.
  • Assert every element and a meaningful minimum length when testing filtered arrays.
  • Test successful creation and validation failures as separate saved requests.
  • Pass generated IDs through a collection variable and convert string values before numeric comparison.
  • Run the saved collection locally with its create request before the dependent read request.

Postman Test Scripts pm.expect Examples are most useful when each assertion proves a specific API behavior. In this tutorial, you will run a small local catalog API, add JavaScript in Postman's Scripts > Post-response tab, and check status codes, headers, JSON values, arrays, errors, and a two-request workflow. Each example names the request it belongs to, so you can paste the script without guessing which response it expects.

You will finish with a collection that runs locally and reports individual test results. The API is deliberately deterministic: item 101 always exists, category filtering has known results, and a created item returns an ID you can reuse. If you are new to the request builder, start with the Postman tutorial for beginners, then return to these assertions.

What You Will Build

  • A local HTTP catalog with health, list, detail, create, and error responses.
  • Saved Postman requests whose post-response scripts use pm.test and pm.expect.
  • Assertions that distinguish status, media type, field type, exact value, and collection invariants.
  • A create-then-read workflow that passes a generated ID through a collection variable.
  • A local Collection Runner result with all tests passing and a controlled failing assertion for debugging practice.

Prerequisites

Use Postman desktop v12 and Node.js v22.0.0 or later. These are published major and release versions; use the latest patch available for your installed branch instead of guessing a patch number. Record the exact Postman version shown in Help > About Postman and the exact Node version shown by node --version in your test notes. This tutorial uses only Node's built-in node:http module, so there are no npm packages or package pins. The current interface calls the old Tests area Scripts > Post-response. Postman's post-response script documentation describes where scripts run and where to read their results.

Check your local tools before writing requests:

node --version
curl --version

The first command should report v22 or a later major version. curl is used only to verify the local API outside Postman. Create a working directory named postman-pm-expect-lab; the file in Step 1 goes there. Run Postman desktop on the same computer as the API. A cloud monitor cannot reach your machine's 127.0.0.1, and the Postman web app may need the desktop agent to reach a local server.

Tool or place Purpose in this tutorial What to verify
Terminal Starts the fixture API node --version works
Postman desktop Sends saved requests and executes scripts Help > About shows the installed version
Scripts > Post-response Holds pm.test calls Test Results appears after Send
Collection Runner, Local Runs the saved workflow in order All named tests pass

Use a dedicated local collection rather than a shared production workspace. The fixture's created records live only in memory and disappear when you stop its process. No credentials are needed, and requests never modify an external service.

Step 1: Start a Deterministic API Fixture

Create server.mjs in postman-pm-expect-lab with the complete server below. It binds to the loopback interface on port 4010. The three seed items remain stable across runs, while each valid POST receives a new numeric ID. The server sends JSON even for validation failures, which lets you test both the status and the error body.

import { createServer } from 'node:http';

const items = [
  { id: 101, name: 'Torque Wrench', category: 'tools', price: 45 },
  { id: 102, name: 'Hex Key Set', category: 'tools', price: 12 },
  { id: 103, name: 'Safety Glasses', category: 'safety', price: 8 },
];
let nextId = 201;

function send(res, status, body, headers = {}) {
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    ...headers,
  });
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:4010');

  if (req.method === 'GET' && url.pathname === '/health') {
    return send(res, 200, { status: 'ok', service: 'catalog' });
  }

  if (req.method === 'GET' && url.pathname === '/items') {
    const category = url.searchParams.get('category');
    const selected = category
      ? items.filter((item) => item.category === category)
      : items;
    return send(res, 200, { items: selected, count: selected.length });
  }

  const match = /^\/items\/(\d+)$/.exec(url.pathname);
  if (req.method === 'GET' && match) {
    const id = Number(match[1]);
    const item = items.find((entry) => entry.id === id);
    return item
      ? send(res, 200, item)
      : send(res, 404, { error: 'ITEM_NOT_FOUND', id });
  }

  if (req.method === 'POST' && url.pathname === '/items') {
    let raw = '';
    for await (const chunk of req) raw += chunk;
    let input;
    try {
      input = JSON.parse(raw);
    } catch {
      return send(res, 400, { error: 'INVALID_JSON' });
    }
    if (
      !input || typeof input.name !== 'string' ||
      input.name.trim() === '' ||
      !['tools', 'safety'].includes(input.category) ||
      typeof input.price !== 'number' || input.price < 0
    ) {
      return send(res, 422, { error: 'VALIDATION_ERROR' });
    }
    const item = {
      id: nextId++,
      name: input.name,
      category: input.category,
      price: input.price,
    };
    items.push(item);
    return send(res, 201, item, { Location: `/items/${item.id}` });
  }

  send(res, 404, { error: 'ROUTE_NOT_FOUND' });
});

server.listen(4010, '127.0.0.1', () => {
  console.log('Catalog API listening on http://127.0.0.1:4010');
});

Verify Step 1: Run node server.mjs and leave that terminal open. In a second terminal run:

curl -i http://127.0.0.1:4010/health

Expect HTTP 200, a Content-Type containing application/json, and {"status":"ok","service":"catalog"}. If port 4010 is occupied, free it before proceeding so every request URL below stays consistent. The send helper also makes response behavior easy to audit: no endpoint silently returns HTML on an error path.

Step 2: Postman Test Scripts pm.expect Examples for Status and Headers

Create a collection named Catalog pm.expect lab. Add a request named Health, set the method to GET and the URL to http://127.0.0.1:4010/health, and save it in that collection. In its Scripts > Post-response tab, paste this entire script:

pm.test('Health returns HTTP 200', function () {
  pm.expect(pm.response.code).to.equal(200);
});

pm.test('Health is JSON', function () {
  pm.expect(pm.response.headers.get('Content-Type'))
    .to.include('application/json');
});

pm.test('Health identifies the catalog service', function () {
  const body = pm.response.json();
  pm.expect(body.status).to.equal('ok');
  pm.expect(body.service).to.equal('catalog');
});

pm.test(name, function) registers a named test. pm.expect uses Chai assertion syntax, so .to.equal(200) compares the actual status code with the expected number. The header assertion uses .include because Content-Type also contains a charset. Exact equality with just application/json would reject a valid application/json; charset=utf-8 response. The JSON assertions check the payload semantics after the transport checks.

Click Send, then open Test Results under the response. You should see three passed tests. Postman's test examples confirm the pm.response.code, pm.response.headers.get, and pm.response.json() APIs used here. A response time assertion can be useful for a known environment, but a strict millisecond limit on a developer laptop is noisy. If your team has an agreed local budget, add a separately named test using pm.expect(pm.response.responseTime).to.be.below(1000) and document why 1000 ms is appropriate for that environment.

Verify Step 2: Send Health twice. Both runs should show three passes. In another terminal, curl -i http://127.0.0.1:4010/health should show the same status and header, giving you an independent check if Postman's UI reports something unexpected. Temporarily change the first expected code to 201, Send, observe one failure, then restore 200. That deliberate failure proves the test actually executes.

Step 3: Assert a JSON Object Without Overfitting

Add Get item 101 as GET http://127.0.0.1:4010/items/101. Its response is a single object with id, name, category, and price. Assert the fields your consumer needs, including their types. Do not use one giant string comparison against the whole response body; whitespace, property order, or a harmless additional field would make that brittle.

pm.test('Item 101 returns HTTP 200', function () {
  pm.expect(pm.response.code).to.equal(200);
});

pm.test('Item 101 has the expected identity', function () {
  const item = pm.response.json();
  pm.expect(item).to.be.an('object');
  pm.expect(item.id).to.equal(101);
  pm.expect(item.name).to.equal('Torque Wrench');
});

pm.test('Item 101 has a usable price and category', function () {
  const item = pm.response.json();
  pm.expect(item.category).to.be.oneOf(['tools', 'safety']);
  pm.expect(item.price).to.be.a('number');
  pm.expect(item.price).to.be.at.least(0);
});

The exact name proves you fetched the intended fixture, while the category and price checks express a broader client contract. oneOf allows either supported category because that test is about valid category values, not whether this item belongs to a particular group. If a category regression matters specifically for item 101, change that assertion to .to.equal('tools') and name the test accordingly. Pick exactness to match the behavior you need to protect.

Verify Step 3: Send the saved request and confirm three passes. Compare its body with curl -s http://127.0.0.1:4010/items/101; the ID must be 101 and the price must be numeric. Try changing the expected name to Safety Glasses once, inspect the failed assertion, then restore the fixture name.

Step 4: Postman Test Scripts pm.expect Examples for Arrays and Filters

Add List tools as GET http://127.0.0.1:4010/items?category=tools. The fixture returns two tools before any item is created. Test the count against the array length instead of hard-coding two; this remains correct when Step 5 adds another tool. Check that every returned record actually matches the requested category, not merely that the first element does.

pm.test('Filtered list returns HTTP 200', function () {
  pm.expect(pm.response.code).to.equal(200);
});

pm.test('Count describes the returned array', function () {
  const body = pm.response.json();
  pm.expect(body.items).to.be.an('array');
  pm.expect(body.count).to.equal(body.items.length);
  pm.expect(body.items.length).to.be.at.least(2);
});

pm.test('Every listed item is a tool with a numeric ID', function () {
  const { items } = pm.response.json();
  items.forEach((item) => {
    pm.expect(item.category).to.equal('tools');
    pm.expect(item.id).to.be.a('number');
  });
});

The length lower bound catches an empty or partial list, while count === items.length catches a mismatch between pagination metadata and the actual array. On a paginated production API, count might mean total matching rows rather than current page size. Read that API's contract before reusing this exact comparison. The loop checks all rows, so a misplaced safety item cannot hide after a valid first record.

There is one subtle assertion trap here: forEach over an empty array executes zero times, so the category test alone would pass vacuously. The separate minimum-length test closes that gap. Likewise, if you test a filter that may legitimately return zero results, assert that the requested empty result is intentional and do not copy the at.least(2) rule. The test should reflect the endpoint's documented behavior, not a generic favorite assertion.

Verify Step 4: Send List tools and expect three passes. Check the independent payload with curl -s 'http://127.0.0.1:4010/items?category=tools'. It should initially show IDs 101 and 102 with count 2. After you create more tools, a higher count is expected and these assertions should still pass.

Step 5: Test Creation, Location, and Validation

Add Create tool as POST http://127.0.0.1:4010/items. Choose Body > raw > JSON and enter the object below. The fixture accepts nonnegative numeric prices and one of two categories. Save the request so Collection Runner can replay it.

{
  "name": "Calibration Gauge",
  "category": "tools",
  "price": 19
}

Place this script in Create tool's Scripts > Post-response tab:

pm.test('Create returns HTTP 201', function () {
  pm.expect(pm.response.code).to.equal(201);
});

pm.test('Created tool echoes accepted fields', function () {
  const item = pm.response.json();
  pm.expect(item.id).to.be.a('number');
  pm.expect(item.name).to.equal('Calibration Gauge');
  pm.expect(item.category).to.equal('tools');
  pm.expect(item.price).to.equal(19);
});

pm.test('Location points to the created item', function () {
  const item = pm.response.json();
  pm.expect(pm.response.headers.get('Location'))
    .to.equal(`/items/${item.id}`);
});

A 201 response alone does not show that the server stored the requested values. These tests verify the returned representation and the relative Location URI. The created ID is deliberately not fixed: it starts at 201 after a fresh server launch and increments for every successful create. Re-running a collection should not fail because the ID changed. If your real API returns an absolute Location URL, compare its parsed path or the full documented URL, rather than assuming this fixture's relative format.

Now add Reject invalid tool as a separate POST to the same URL. In Body > raw > JSON enter {"name":"","category":"tools","price":-1}. The body is syntactically valid JSON but fails the server's validation rules. Give this request its own post-response script:

pm.test('Invalid tool returns HTTP 422', function () {
  pm.expect(pm.response.code).to.equal(422);
});

pm.test('Validation error has a stable code', function () {
  const body = pm.response.json();
  pm.expect(body.error).to.equal('VALIDATION_ERROR');
  pm.expect(body).to.not.have.property('id');
});

Use a separate request for the negative case. A successful create and an intentionally invalid create have different expected statuses, so combining both into one request and accepting either 201 or 422 would conceal a real regression. The error code is more stable for clients than human-readable prose; the absence of an ID confirms this path did not report a created record.

Verify Step 5: Send each request and expect three passes for Create tool and two for Reject invalid tool. You can reproduce the successful response from a terminal with curl -i -X POST http://127.0.0.1:4010/items -H 'Content-Type: application/json' -d '{"name":"Calibration Gauge","category":"tools","price":19}'. That curl command creates an extra record, so the next successful ID will increase. Use curl -i with the invalid JSON body to confirm the 422 path independently.

Step 6: Prove Missing Resources Fail Correctly

Add Get missing item as GET http://127.0.0.1:4010/items/999. The server should return 404 with an error code and the numeric ID that was requested. This tests an outcome that a happy-path suite misses: a missing item must not be represented as a valid item, an empty 200 response, or a generic route error.

pm.test('Missing item returns HTTP 404', function () {
  pm.expect(pm.response.code).to.equal(404);
});

pm.test('Missing item has the expected error contract', function () {
  const body = pm.response.json();
  pm.expect(body.error).to.equal('ITEM_NOT_FOUND');
  pm.expect(body.id).to.equal(999);
  pm.expect(body).to.not.have.property('name');
});

For this endpoint, ITEM_NOT_FOUND differs from ROUTE_NOT_FOUND. The former means the route exists but the requested record does not. That distinction helps callers decide whether to change the identifier or fix a malformed URL. A Postman test that only checks 404 would miss a route typo such as /item/999; the body assertion makes the failure more precise.

Do not treat every non-200 code as a test failure. The expected result for this request is a 404, so a passing test means the service handled the negative case correctly. In a larger suite, name the request and test so the run report does not make an intentional error response look like an incident. For deeper scenario selection, see the API testing roadmap.

Verify Step 6: Send Get missing item and expect two passes. Then run curl -i http://127.0.0.1:4010/items/999; its JSON body should contain ITEM_NOT_FOUND and numeric 999. Change the request path to /item/999 temporarily: the status still reads 404, but the error-contract test should fail with ROUTE_NOT_FOUND. Restore /items/999 before the collection run.

Step 7: Chain a Created ID Into a Read Request

The previous POST verifies a single response. Now prove the created resource is readable. At the bottom of the Create tool post-response script from Step 5, append this named test and variable assignment. Keep the three original tests above it. The assignment only happens when the POST returned 201, so an error response cannot overwrite a good ID with undefined.

pm.test('Created ID is ready for the next request', function () {
  pm.expect(pm.response.code).to.equal(201);
  const item = pm.response.json();
  pm.expect(item.id).to.be.a('number');
  pm.collectionVariables.set('createdItemId', String(item.id));
});

Add Read created tool as GET http://127.0.0.1:4010/items/{{createdItemId}} and save it immediately after Create tool in the collection. Paste this script in its Post-response tab:

pm.test('Created tool is readable', function () {
  pm.expect(pm.response.code).to.equal(200);
  const item = pm.response.json();
  pm.expect(item.id).to.equal(Number(pm.collectionVariables.get('createdItemId')));
  pm.expect(item.name).to.equal('Calibration Gauge');
  pm.expect(item.price).to.equal(19);
});

Postman stores variable values as strings, so String(item.id) makes the stored type explicit and Number(...) converts it back before comparing to the response's numeric ID. The request URL uses double braces for substitution. Collection scope is appropriate because both requests belong to the same collection; a global variable would increase the risk of a stale value from another project. Learn the precedence rules in Postman collection variables and scopes before layering environment or data variables with the same name.

Run Create tool, then Read created tool manually. Next open the collection's Run action, select a Functional, Local run, and select these requests in order: Health, Get item 101, List tools, Create tool, Read created tool, Reject invalid tool, Get missing item. Set one iteration and start the run. The Collection Runner documentation explains the current run controls and result view. Check that every named test passes; the intentional 404 and 422 responses should still produce passing assertions.

Verify Step 7: After Create tool, inspect the collection variable createdItemId and confirm it contains digits. Send Read created tool and expect one pass and the same ID in the response. As an independent terminal check, copy that numeric ID into this command and expect HTTP 200 with Calibration Gauge:

curl -i http://127.0.0.1:4010/items/201

The command shows 201 as an example after a fresh server start with one successful POST. Replace it with the actual createdItemId whenever other successful creates have advanced the counter. The local runner should show zero failed tests for one iteration. If Read created tool runs first, the URL may contain an unresolved variable or an old value; move it after the create request and run again. The server process must remain open throughout the run.

Troubleshooting

  • Problem: ECONNREFUSED or no response from 127.0.0.1 -> Start node server.mjs in the working directory and confirm curl -i http://127.0.0.1:4010/health works. In the web app, use an agent that can reach localhost, or run the desktop app locally.
  • Problem: pm.response.json() throws a parse error -> Inspect the raw body and Content-Type. A server crash page, proxy login page, or typo in the URL may return HTML. Fix the request or server response before treating it as JSON.
  • Problem: expected undefined to equal ... -> Inspect the actual response shape and spelling, then check that the script is attached to the correct request. A missing body.error on a success response usually means the wrong request or expected status.
  • Problem: {{createdItemId}} is unresolved -> Save both requests in the same collection, run Create tool first, and inspect the collection variable. A failed POST should not be used to seed the read request.
  • Problem: List count changes after testing POST -> Each valid create adds an in-memory item. Keep the invariant count === items.length; restart the server only if you want the initial three-item fixture again.
  • Problem: Tests appear absent after Send -> Put the code in Scripts > Post-response, save the request, and open Test Results. Check the Postman Console for JavaScript errors if a script stops before registering all its tests.

Interview Questions and Answers

A practical interview answer should explain what an assertion protects, when it is too strict, and how you would diagnose a failure. The interviewQnA entries below give six concise model answers. Practice them by changing one fixture response or one expected value, then reading the named failure in Postman. For broader prompts, review Postman interview questions.

Common Mistakes and Better Checks

  • Treating any 2xx response as proof of correct behavior. Add field and type assertions for data a client actually consumes.
  • Using .equal on arrays or objects when structural equality is intended. For a full nested comparison, use Chai's .deep.equal; for a stable subset, assert only the relevant fields.
  • Assuming an array loop guarantees content. Assert a meaningful length before forEach when an empty result would be a bug.
  • Hard-coding generated IDs or exact timing on a local machine. Read IDs from the response and use time budgets only when the environment supports a credible threshold.
  • Parsing a body without checking which response arrived. A clear status test and a JSON media-type check make an unexpected HTML response easier to diagnose.
  • Storing workflow state in a global variable. Prefer collection or environment scope and clean up values that should not persist across independent runs.

The important habit is to tie every assertion to an API promise. If the contract says a field is optional, do not turn it into a required field just because today's fixture includes it. If the contract says a value must be stable, do not weaken the check to oneOf simply to avoid a failure. This is how a short Postman script becomes a trustworthy regression test.

Where To Go Next

You now have working Postman Test Scripts pm.expect Examples for single responses and a stateful workflow. Export the collection if you want to review it in source control, then keep the local server running while you repeat the Collection Runner test. Record the installed Postman and Node versions alongside any failure report so another engineer can reproduce the environment.

Expand the suite with Postman pre-request scripts when a request needs a fresh input before sending. Use Postman data-driven testing to run the same contract over several input rows, and Postman Newman in CI if your team uses that runner in a build pipeline. Start by adding one new negative request, such as malformed JSON expecting 400 and INVALID_JSON, then verify it passes locally before increasing the suite's scope.

Interview Questions and Answers

What is the difference between pm.test and pm.expect?

pm.test registers a named test and reports its result in Postman. pm.expect makes an assertion inside that test using Chai syntax. I use several named tests when status, headers, and payload represent separate failure signals.

Why would you check Content-Type with include rather than equal?

A valid JSON response can include a charset parameter such as application/json; charset=utf-8. An exact comparison to application/json would reject it. I use include when the contract permits parameters and an exact assertion only when the full header value is specified.

How do you prevent a list assertion from passing on an empty array?

An array forEach executes no callbacks on an empty list, so element assertions can pass vacuously. I first assert a meaningful minimum length or exact expected count, then inspect every element. For a legitimately empty search result, I write a separate test for that behavior.

What do you test for a create endpoint beyond HTTP 201?

I check the returned ID type, echoed or canonicalized fields, and the Location header when the contract provides one. Then I read the generated resource by ID to prove it was persisted. I avoid assuming the generated ID has a fixed value.

How do you pass data between two requests in a Postman collection?

I save the value in a collection variable after a successful first response and reference it with double braces in the next request URL. Because variables are stored as strings, I convert types when comparing them with JSON numbers. I also enforce the request order in Collection Runner.

How would you distinguish an intended 404 from a typo in the route?

I assert both the 404 status and a documented error code for the missing resource. A route typo may also return 404 but with a different error body. That second assertion keeps the negative test specific.

When would you use deep.equal in a Postman assertion?

I use deep.equal when the full nested structure is contractual and order matters as defined. For evolving responses, I prefer focused field and type assertions so unrelated additions do not break tests. I avoid equal for distinct object instances because it compares reference identity.

Frequently Asked Questions

Where do I put pm.expect code in Postman?

Open the saved request and select Scripts > Post-response. Put pm.expect assertions inside named pm.test blocks, send the request, and read the Test Results tab under the response.

How do I check a status code with pm.expect?

Use pm.test with pm.expect(pm.response.code).to.equal(200), replacing 200 with the status your request should produce. Keep an expected 404 or 422 as a separate request so failures are unambiguous.

How do I assert a JSON field in a Postman test script?

Call pm.response.json() to parse the body, then assert a property such as pm.expect(body.status).to.equal("ok"). Confirm that the endpoint returns JSON before relying on the parser.

Why does pm.expect report undefined in my test?

The property may be absent, misspelled, nested differently, or read from the wrong request response. Inspect the actual body and status, then update the property path or fix the request.

How do I compare a generated ID across Postman requests?

Store the ID after a successful create with pm.collectionVariables.set, reference it as {{createdItemId}} in the next URL, and read it with pm.collectionVariables.get. Convert the stored string to a number before comparing it to a numeric JSON ID.

Should I assert an exact response time in Postman?

Only when you have a justified budget and a controlled environment. Local laptop timing and shared services vary, so an arbitrary low limit can create noise instead of a useful regression signal.

Can a Postman test pass for an expected 404 response?

Yes. If the scenario is a missing resource, assert 404 and its documented error body. A passing test means the API handled that negative case as specified.

Related Guides