Writing tests in Postman
pm.test and pm.expect for status, headers, body, timing and a JSON schema.
Sending a request and looking at the response is checking. Writing down what the response must look like, so Postman checks it every time, is testing. Postman tests are short JavaScript snippets that run after each response arrives. They're the difference between a collection of saved requests and a test suite.
Where tests go
Every request, folder and collection has a Scripts tab with two parts: Pre-request, which runs before the request is sent, and Post-response, which runs after the response arrives. Older Postman versions called the second one Tests and gave it its own tab. Tests go in Post-response. A script on a folder or collection runs for every request inside it, which is handy for checks every response should pass.
After a request runs, its results appear in the Test Results tab of the response, one line per test, passed or failed.
pm.test and pm.expect
pm.test(name, fn) declares one test. If the function throws, the test fails, and the name is what you see in the results, so write names as sentences that say what should be true. Inside, pm.expectmakes assertions. It's the Chai library, so it reads like English: to.eql, to.be.above, to.include, to.be.within. Here is the folder 04 Tests / Product 1, fully checked:
pm.test("status is 200", () => {
pm.response.to.have.status(200);
});
pm.test("the body is JSON", () => {
pm.response.to.have.header("Content-Type");
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});
const product = pm.response.json();
pm.test("it is product 1, with a positive price", () => {
pm.expect(product.id).to.eql(1);
pm.expect(product.price).to.be.a("number").and.above(0);
});
pm.test("the rating is between 0 and 5", () => {
pm.expect(product.rating.rate).to.be.within(0, 5);
});
pm.test("it answers in under 2 seconds", () => {
pm.expect(pm.response.responseTime).to.be.below(2000);
});Each test checks one idea, and they're ordered the way you'd read a response: status, then headers, then the body, then timing. When one fails, its name says exactly what broke. A single test called response is OK holding all those assertions would stop at the first failure and tell you much less.
| To check | Write |
|---|---|
| Status | pm.response.to.have.status(200) |
| A header exists | pm.response.to.have.header("Content-Type") |
| A header's value | pm.expect(pm.response.headers.get("X-Total-Count")).to.eql("6") |
| A field | pm.expect(pm.response.json().id).to.eql(1) |
| A type | pm.expect(product.price).to.be.a("number") |
| Text in the body | pm.expect(pm.response.text()).to.include("Headphones") |
| Response time | pm.expect(pm.response.responseTime).to.be.below(2000) |
Postman's script editor also has a snippets list that inserts the common ones for you. They're a fine start, but rename the tests they create: Status code is 200 repeated across fifty requests tells you nothing when one fails.
Checking the whole shape with a schema
Field-by-field checks miss fields you didn't think of. A JSON Schema describes the whole shape: which fields must exist and what type each one is. Postman checks a response against one with jsonSchema:
const schema = {
type: "object",
required: ["id", "title", "price", "category", "rating", "inStock"],
properties: {
id: { type: "integer" },
title: { type: "string", minLength: 1 },
price: { type: "number" },
category: { type: "string" },
rating: {
type: "object",
required: ["rate", "count"],
properties: { rate: { type: "number" }, count: { type: "integer" } },
},
inStock: { type: "boolean" },
},
};
pm.test("the body matches the product schema", () => {
pm.response.to.have.jsonSchema(schema);
});If a later release turns price into a string, or drops inStock, this test fails even though no other test mentions those fields. The REST tutorial's lesson on schemas and contract testing goes further into writing schemas.
Tests for failures
A suite that only checks success misses half the behaviour. The request A missing product returns 404 asks for a product that doesn't exist and expects that:
pm.test("status is 404", () => pm.response.to.have.status(404));
pm.test("the error names the missing ID", () => {
pm.expect(pm.response.json().message).to.include("9999");
});Note that this test passes on a 404. Postman doesn't treat 4xx or 5xx responses as failures on their own; only your tests decide what counts as wrong. That's what lets you test error cases at all, and it's also why a request with no tests never fails, whatever comes back.
Tests for lists
Lists need different checks, because the exact contents change. This test API is shared, and its data changes as people use it: there are more electronics products now than when the seed data was written. So the tests for A filtered page of products check properties that must always hold, not exact values:
const products = pm.response.json();
pm.test("no more items than the limit", () => {
pm.expect(products.length).to.be.at.most(2);
});
pm.test("every item is in the requested category", () => {
products.forEach((p) => pm.expect(p.category).to.eql("electronics"));
});
pm.test("X-Total-Count is a number at least as large as the page", () => {
const total = Number(pm.response.headers.get("X-Total-Count"));
pm.expect(total).to.be.at.least(products.length);
});A test asserting X-Total-Count equals 5 would have passed on the seed data and failed the day someone added a product. Assert what the API promises (no more than the limit, every item matches the filter, the total is consistent with the page), and save exact-value checks for data your tests created themselves.
Running the tests
Here are all three requests in the folder, run with Newman; Postman's Collection Runner shows the same:
□ 04 Tests
└ Product 1, fully checked
GET https://api.testingapis.com/api/Products/1 [200 OK, 579B, 163ms]
√ status is 200
√ the body is JSON
√ it is product 1, with a positive price
√ the rating is between 0 and 5
√ it answers in under 2 seconds
√ the body matches the product schema
└ A missing product returns 404
GET https://api.testingapis.com/api/Products/9999 [404 Not Found, 349B, 33ms]
√ status is 404
√ the error names the missing ID
└ A filtered page of products
GET https://api.testingapis.com/api/Products?category=electronics&limit=2&page=1 [200 OK, 938B, 76ms]
√ no more items than the limit
√ every item is in the requested category
√ X-Total-Count is a number at least as large as the pageAnd here is what a failure looks like, from a run where a test expected the wrong category:
1. AssertionError product 2 is in books
iteration: 2 expected 'electronics' to deeply equal 'books'
at assertion:1 in test-script
inside "06 Data-driven / Product by ID, from the data file"The test name, the expected and actual values, and the request it happened in. That's enough to start looking without opening Postman, which matters once these tests run in CI and the first person to see a failure is reading a build log.
Test checklist
- Every request has at least one test; a request without tests can't fail.
- One idea per test, named as a sentence.
- Status, headers and body are all checked, not just the body.
- At least one schema check per kind of resource.
- Error cases have their own requests and tests.
- Lists are tested for properties, not exact counts, unless the test created the data.
Exercise
Add tests to your own Get one product request from lesson 1: the status, the content type, that image is a URL starting with https://, and that createdAt is a date in the past. Then break each test on purpose, one at a time, and read the failure message. Is each one clear enough that a colleague could act on it without asking you?