Level 2 · Mid-Level Automation · Practice 02

API Testing with Postman + Newman

Build a small REST collection in the Postman desktop app, save it as JSON, then run it headless from the command line with Newman.

1 Goal

By the end of this exercise you will have a four-request Postman collection targeting jsonplaceholder.typicode.com — a free public REST API for testing. Each request will have automated assertions (status code, response shape, latency). You will export the collection to a JSON file, then run it without opening Postman at all using Newman, Postman's free CLI runner. That last step is the one that matters: your CI pipeline does not have a UI, so your API tests must run headless.

Time: about 30 minutes. Free tools only.

2 Install Postman & Newman

You need two separate things: the Postman desktop app (for authoring) and the Newman CLI (for running headless). Both are free.

  1. Install the Postman desktop app Download from postman.com/downloads. The free tier is all you need; no account is required to author or run collections locally, though signing in with a free account enables sync.

    Run the .exe installer. It installs to %LOCALAPPDATA%\Postman. Launch Postman and click Skip and go to the app on the sign-in screen.

    Drag the app into /Applications. First launch may ask for permission to access the keychain — you can safely say no for this exercise.

    Use the .tar.gz download, or on Ubuntu/Debian install the snap:

    sudo snap install postman
  2. Install Node.js if you have not already Newman runs on Node.js. If you completed Practice 01, you are set. Otherwise grab the LTS installer from nodejs.org/en/download and verify:
    node --version
    npm --version
    Expect v20.11.x or newer for Node, and 10.x or newer for npm.
  3. Install Newman globally via npm
    npm install -g newman

    Run the install in an elevated PowerShell (right-click → Run as administrator) if you hit a permissions error.

    If you get EACCES, prepend sudo. Long-term, a better fix is to point npm's global prefix at a folder you own (npm config set prefix ~/.npm-global).

    Same EACCES advice. Using a Node version manager (nvm, fnm) avoids sudo entirely.

  4. Verify Newman
    newman --version
    You should see something like 6.2.1. If the command is not found, close and reopen the terminal — npm's global bin folder needs to be on your PATH.
Why two tools? Postman the app is for designing and debugging interactively. Newman is the headless runner that executes the same collection from CI. Tests authored in one run identically in the other — that is the whole point.

3 Project setup

Create a folder to hold your exported collection and, later, the Newman run report.

mkdir api-tests
cd api-tests

Final tree you are working towards:

api-tests/ ├── jsonplaceholder.postman_collection.json (exported from Postman) └── newman-report.html (generated when you run Newman)

Now open Postman and create a new collection:

  1. Open Postman and create a workspace Left sidebar → WorkspacesCreate Workspace. Name it bootcamp, visibility Personal. Click Create.
  2. Create a collection Left sidebar → Collections tab → click the + button. Rename the collection to JSONPlaceholder (double-click the name, type, press Enter).
  3. Add a collection-level variable for the base URL Click the collection name → Variables tab. Add one row:
    VARIABLE     INITIAL VALUE                        CURRENT VALUE
    baseUrl      https://jsonplaceholder.typicode.com https://jsonplaceholder.typicode.com
    Click Save. You will reference this as {{baseUrl}} in every request. Changing it in one place reconfigures the whole collection — the same principle as the Page Object Model for APIs.

4 Build the collection

Add four requests to the JSONPlaceholder collection. For each one: right-click the collection → Add request, set the method and URL, then paste the test script into the Tests tab.

Request 1: GET all posts

Method: GET · URL: {{baseUrl}}/posts

Tests tab — paste into Postman
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Response is an array of 100 posts", function () {
    const body = pm.response.json();
    pm.expect(body).to.be.an("array");
    pm.expect(body).to.have.lengthOf(100);
});

pm.test("Response time is under 2 seconds", function () {
    pm.expect(pm.response.responseTime).to.be.below(2000);
});

Request 2: GET a single post

Method: GET · URL: {{baseUrl}}/posts/1

Tests tab
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Post has the expected shape", function () {
    const body = pm.response.json();
    pm.expect(body).to.have.property("userId");
    pm.expect(body).to.have.property("id", 1);
    pm.expect(body).to.have.property("title").that.is.a("string");
    pm.expect(body).to.have.property("body").that.is.a("string");
});

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

Request 3: POST a new post

Method: POST · URL: {{baseUrl}}/posts · Headers: Content-Type: application/json

Body tab → raw → dropdown set to JSON. Paste:

Body
{
  "title": "Bootcamp practice",
  "body": "Posted from Newman",
  "userId": 1
}
Tests tab
pm.test("Status code is 201 Created", function () {
    pm.response.to.have.status(201);
});

pm.test("Response echoes the title we sent", function () {
    const body = pm.response.json();
    pm.expect(body.title).to.eql("Bootcamp practice");
    pm.expect(body.userId).to.eql(1);
});

pm.test("Response includes a new id", function () {
    const body = pm.response.json();
    pm.expect(body.id).to.be.a("number");
    pm.collectionVariables.set("createdPostId", body.id);
});

The final line stores the new post's id in a collection variable so the next request can reuse it.

Request 4: GET the post we just created

Method: GET · URL: {{baseUrl}}/posts/{{createdPostId}}

Tests tab
pm.test("Status code is 200 or 404 (see note)", function () {
    pm.expect([200, 404]).to.include(pm.response.code);
});

pm.test("If 200, the response has our id", function () {
    if (pm.response.code === 200) {
        const body = pm.response.json();
        pm.expect(body.id).to.eql(Number(pm.collectionVariables.get("createdPostId")));
    }
});
Heads up: JSONPlaceholder is a fake API. It accepts your POST and returns a sensible response, but it does not actually persist anything. That is why Request 4 tolerates both 200 and 404. In a real API test against a staging environment, you would expect 200 and assert the persisted data exactly matches what you posted.

Export the collection

  1. Click the three-dot menu next to the collection name → Export Choose Collection v2.1, click Export, and save to your api-tests/ folder as jsonplaceholder.postman_collection.json.
  2. Sanity-check the file Open it in a text editor. You should see JSON with an item array containing your four requests and their event blocks (that is where Postman stores your test scripts).

That JSON file is now a portable, version-controllable artifact. Commit it alongside your code. Any teammate with Newman can run it without Postman.

5 Run & verify

First, run it inside Postman to make sure your assertions behave. Click the collection → Run button → Run JSONPlaceholder. You should see all eleven assertions pass (green).

Now the important step — run it from the command line with Newman:

newman run jsonplaceholder.postman_collection.json

Expected output (abbreviated):

newman

JSONPlaceholder

→ GET all posts
  GET https://jsonplaceholder.typicode.com/posts [200 OK, 26.1kB, 230ms]
  ✓ Status code is 200
  ✓ Response is an array of 100 posts
  ✓ Response time is under 2 seconds

→ GET a single post
  GET https://jsonplaceholder.typicode.com/posts/1 [200 OK, 480B, 95ms]
  ✓ Status code is 200
  ✓ Post has the expected shape
  ✓ Content-Type is JSON

→ POST a new post
  POST https://jsonplaceholder.typicode.com/posts [201 Created, 420B, 110ms]
  ✓ Status code is 201 Created
  ✓ Response echoes the title we sent
  ✓ Response includes a new id

→ GET the post we just created
  GET https://jsonplaceholder.typicode.com/posts/101 [404 Not Found, 180B, 85ms]
  ✓ Status code is 200 or 404 (see note)
  ✓ If 200, the response has our id

┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊
                         executed    failed
            iterations         1         0
              requests         4         0
          test-scripts         8         0
    prerequest-scripts         4         0
           assertions        11         0

Eleven assertions, zero failures. That is what passing looks like.

Bonus: generate an HTML report

npm install -g newman-reporter-htmlextra
newman run jsonplaceholder.postman_collection.json -r htmlextra --reporter-htmlextra-export newman-report.html

Open newman-report.html in a browser. You get a full timeline, request/response bodies, and assertion status — this is the report you attach to a CI build.

CI-ready in one line: any CI runner (GitHub Actions, Jenkins, GitLab, Buildkite) can run newman run path/to/collection.json. A non-zero exit code fails the build. No browser, no display server, no licences.

6 Troubleshooting

Error: "newman: command not found" (or "not recognized" on Windows)

npm's global bin directory is not on your PATH. Three fixes, in order:

  1. Close and reopen the terminal — PATH only updates for new sessions.
  2. Run npm config get prefix and add <prefix>/bin (macOS/Linux) or <prefix> (Windows) to PATH manually.
  3. Run Newman via npx instead: npx newman run collection.json. It works without global install.
Postman's Run button passes, but Newman fails with "Could not get any response"

Postman the desktop app uses your OS proxy settings by default; Newman does not. If you are on a corporate network, try:

newman run collection.json --insecure --ssl-client-cert-list none

If your proxy needs auth, pass it explicitly:

newman run collection.json --proxy http://user:pass@proxy.corp:8080

For this exercise against a public API, the simplest workaround is to run it off the corporate VPN.

"TypeError: pm.expect(...).to.have.lengthOf is not a function"

You are on a very old Newman. Postman's test library (Chai) shipped lengthOf years ago. Upgrade:

npm install -g newman@latest

Aim for Newman 6.x or newer.

Request 3 succeeds but createdPostId is empty in Request 4

Two common causes:

  1. The pm.collectionVariables.set(...) line ran inside an if block that was skipped because an earlier assertion failed. Open the Postman Console (View menu → Show Postman Console) to see the actual flow.
  2. You set an environment variable instead of a collection variable, or vice versa. They are different scopes. Use pm.collectionVariables on both sides (setter and {{createdPostId}} lookup).

7 Challenge

Add environments and data-driven runs

Easy: Add a fifth request — DELETE {{baseUrl}}/posts/1 — with assertions for a 200 status code and an empty response body. Re-export the collection and run it with Newman. All 13 assertions should pass.

Harder: Create a Postman environment with baseUrl pointing at reqres.in (another free public API) and adjust one request to hit {{baseUrl}}/api/users/2. Export the environment to reqres.postman_environment.json. Run it with:

newman run collection.json -e reqres.postman_environment.json

Then make the collection data-driven: create a users.csv with a userId column, reference {{userId}} in a request URL, and run:

newman run collection.json -d users.csv

Newman will iterate the collection once per CSV row — that's true data-driven API testing from the command line.