Pre-request scripts and request flows
Random test data, a signed request, a create-read-delete chain, and polling until a job finishes.
Tests run after a response. Pre-request scripts run before the request goes out, and can change it: work out a value, add a header, sign the body, or decide to skip the request altogether. Combined with variables, they turn a list of requests into a flow: create something, check it, use it, clean it up. Every script in this lesson is in the folder 05 Scripts and flows of the tutorial collection, and every one passed against the live API.
Test data nobody else has
A test API shared by many people, or a staging server shared by a team, fills up with records called test. If your test creates a todo called testand then searches for it, it may find someone else's. The fix is data that's unique to the run. This pre-request script builds a title from one of Postman's random-word variables and saves it:
// A title nobody else will have, so the read-back check can't match someone else's todo
const title = "Postman tutorial " + pm.variables.replaceIn("{{$randomLoremWords}}");
pm.collectionVariables.set("todoTitle", title);pm.variables.replaceIn resolves a {{...}} expression inside a script, here a fresh set of random words. The body then uses the saved title rather than the random variable directly:
{
"title": "{{todoTitle}}",
"userId": 1,
"priority": "low"
}Why not put {{$randomLoremWords}} straight into the body? Because every use produces a new value. The body would get one title and the later check would generate another. Saving it to a variable first gives every request in the flow the same value.
A create, read, delete chain
The create request saves the new todo's ID:
pm.test("created", () => pm.response.to.have.status(201));
const todo = pm.response.json().data;
pm.collectionVariables.set("todoId", todo.id);The next request reads it back from {{baseUrl}}/api/Todos/{{todoId}} and compares the title:
pm.test("the stored title is the one we sent", () => {
pm.expect(pm.response.json().title).to.eql(pm.collectionVariables.get("todoTitle"));
});Then a DELETE to the same address, and a final GET that expects 404. Four requests, each depending on the one before:
└ Create a todo with a random title
POST https://api.testingapis.com/api/Todos [201 Created, 433B, 161ms]
√ created
└ Read the todo back
GET https://api.testingapis.com/api/Todos/31 [200 OK, 381B, 26ms]
√ the stored title is the one we sent
└ Delete the todo
DELETE https://api.testingapis.com/api/Todos/31 [200 OK, 298B, 31ms]
√ deleted
└ Check the todo is gone
GET https://api.testingapis.com/api/Todos/31 [404 Not Found, 344B, 32ms]
√ a deleted todo returns 404The read-back is the step people skip and the one that finds bugs. A create response describes what the server meant to store; only a separate read shows what it did store. The REST tutorial's lesson on testing a resource end to end explains why at length. The final 404 matters too: a delete that answers 200 and leaves the record in place is a real and common bug.
Signing a request
Some APIs need a value that can only be worked out at send time, like a signature over the body with the current time. The HMAC endpoint wants exactly that, and a pre-request script can produce it:
// Sign timestamp + "." + body with the shared secret, as the endpoint expects
const cryptoJs = require("crypto-js");
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = pm.request.body.raw;
const signature = cryptoJs.HmacSHA256(timestamp + "." + body, "apibee-hmac-secret").toString(cryptoJs.enc.Hex);
pm.request.headers.upsert({ key: "X-Timestamp", value: timestamp });
pm.request.headers.upsert({ key: "X-Signature", value: signature });pm.request is the request about to be sent, and headers.upsert adds a header or replaces one with the same name. crypto-jsis one of the libraries Postman's script sandbox includes, along with others such as lodash and moment, loaded with require.
Two traps showed up while writing this. Postman also offers CryptoJS as a ready-made global, but Newman reports it as deprecated and points to require("crypto-js"). And writing const CryptoJS = require("crypto-js"), the obvious name, fails with a syntax error, because it clashes with that global. Use any other name. The request signing lesson explains the signature itself.
Waiting for a job to finish
Async APIs accept work and finish it later. The jobs endpoint answers a POST with 202 Accepted and a job ID, and the client checks /api/jobs/{id}sign in to see until the status changes. A Postman run can do the same by repeating a request:
// Unlike "Start a job", the status endpoint returns the job itself, not wrapped in "data"
const job = pm.response.json();
const polls = Number(pm.collectionVariables.get("polls")) + 1;
pm.collectionVariables.set("polls", polls);
if ((job.status === "queued" || job.status === "running") && polls < 30) {
// Not done: run this request again after the delay the server suggests
const wait = Number(pm.response.headers.get("Retry-After") || 1) * 1000;
pm.execution.setNextRequest(pm.info.requestName);
setTimeout(() => {}, wait);
} else {
pm.test("the job completed", () => pm.expect(job.status).to.eql("completed"));
pm.collectionVariables.set("jobResultUrl", job.resultUrl);
}pm.execution.setNextRequesttells the runner which request to run next. Passing this request's own name makes it run again. Older collections usepostman.setNextRequest, which still works but is deprecated.- The
setTimeoutholds the run until the timer finishes, so each poll waits as long as the server'sRetry-Afterheader asks. - The
polls < 30limit stops a job that never finishes from looping forever. Every polling loop needs one.
└ Start a job
POST https://api.testingapis.com/api/jobs [202 Accepted, 597B, 22ms]
√ accepted for processing
└ Poll the job until it finishes
GET https://api.testingapis.com/api/jobs/job_1007 [200 OK, 492B, 34ms]
Attempting to set next request to Poll the job until it finishes
└ Poll the job until it finishes
GET https://api.testingapis.com/api/jobs/job_1007 [200 OK, 492B, 34ms]
...three more polls...
└ Poll the job until it finishes
GET https://api.testingapis.com/api/jobs/job_1007 [200 OK, 540B, 27ms]
√ the job completed
└ Get the job result
GET https://api.testingapis.com/api/jobs/job_1007/result [200 OK, 647B, 21ms]
√ the result is availableSix polls over about five seconds: the job queues for two seconds, then runs for the three it was asked to. Then the result request uses the resultUrl the last poll saved.
Two things to know about this pattern
setNextRequestonly works in the Collection Runner and Newman. When you press Send on a single request, nothing runs next, so the request polls once and stops.- Read the real response before writing the script. The create response wraps the job in
data, but the status endpoint returns the job on its own. The first version of this script assumed both were wrapped and failed withCannot read properties of undefined (reading 'status'). An API whose responses change shape between related endpoints deserves a note in a bug report; a script should be written against what each endpoint actually sends.
Skipping a request
A pre-request script can also decide a request shouldn't run. The data-driven request in the next lesson needs a row of data, so it skips itself when there isn't one, using pm.execution.skipRequest(). That keeps a full run of the collection green when no data file is supplied, instead of failing on a request that had nothing to test.
Exercise
Build a flow for posts: create a post at /api/Posts with a unique title, read it back, change its title with PUT, read it again and check the new title, then delete it and confirm the 404. Use the posts API docs for the fields. Run the folder twice in the Collection Runner. Do both runs pass, and does the second run ever see data from the first?