Level 4 · Test Lead Automation · Practice 02

Postman Collection Runner in CI

Turn a hand-driven Postman collection into a reliable, reportable command your CI can run. Export, run with Newman, and emit JUnit XML that any build server can parse.

1 Goal

Build a Postman collection of four API requests against https://jsonplaceholder.typicode.com, add assertions with Postman's test scripts, export the collection and environment as JSON, then run the exported files on your local machine with the Newman CLI. The same command will be the one a CI server runs, and the JUnit XML output will be the artefact the CI server uses to mark the build green or red. By the end you should be able to argue, concretely, why a Newman-in-CI pattern is cheaper and more maintainable than letting humans click "Run Collection" before every release.

2 Install Postman (free) & Newman (free)

  1. Download the Postman desktop app Postman has a permanently free tier that covers everything in this exercise (collections, environments, test scripts, exports). Download from postman.com/downloads. You can sign up with email or skip the sign-in and work offline — the free tier does not require an account for local work.
  2. Confirm Node.js 18+ is installed Newman is an npm package, so it needs Node. If you already completed Practice 01, you have it. Otherwise install the LTS build from nodejs.org.
    All platforms
    node --version
    npm --version
  3. Install Newman and the JUnit reporter globally Newman is a free, open-source command-line runner maintained by the Postman team. The JUnit reporter is a separate npm package.
    PowerShell
    npm install -g newman newman-reporter-junitfull
    bash/zsh (may need sudo on some setups)
    npm install -g newman newman-reporter-junitfull
  4. Verify Newman is on your PATH
    All platforms
    newman --version
    Expected output: a version number like 6.2.1. If the command is not found on macOS or Linux, add $(npm config get prefix)/bin to your PATH.
Why newman-reporter-junitfull and not junit? The built-in junit reporter only emits top-level request results. junitfull emits one <testcase> per Postman assertion, which is what Jenkins, GitHub Actions, GitLab, and TeamCity expect when they display pass/fail counts.

3 Project setup

  1. Create a project folder
    All platforms
    mkdir newman-lab
    cd newman-lab
    mkdir reports
  2. Target the final layout After step 4 you'll have these files:
    Tree
    newman-lab/
      jsonplaceholder.postman_collection.json
      jsonplaceholder.postman_environment.json
      reports/                   (created by Newman on first run)
        junit.xml

4 Build the collection & export

You have two routes: click through Postman, or paste the ready-made JSON below. Do it in Postman first so you understand each moving part, then confirm the exported JSON matches.

  1. In Postman, create a new collection called jsonplaceholder Add four requests, one per row below. For each, open the Tests tab and paste the matching snippet.
    Postman Tests tab — GET /posts/1
    pm.test("status is 200", function () {
        pm.response.to.have.status(200);
    });
    pm.test("response has userId", function () {
        const body = pm.response.json();
        pm.expect(body).to.have.property("userId");
        pm.expect(body.id).to.eql(1);
    });
    Postman Tests tab — GET /posts
    pm.test("status is 200", function () {
        pm.response.to.have.status(200);
    });
    pm.test("returns an array of at least 100 items", function () {
        const body = pm.response.json();
        pm.expect(body).to.be.an("array");
        pm.expect(body.length).to.be.at.least(100);
    });
    Postman Tests tab — POST /posts
    pm.test("status is 201", function () {
        pm.response.to.have.status(201);
    });
    pm.test("echoes back the payload", function () {
        const body = pm.response.json();
        pm.expect(body.title).to.eql("resync-lab");
        pm.expect(body.userId).to.eql(1);
    });
    Postman Tests tab — GET /users/1
    pm.test("status is 200", function () {
        pm.response.to.have.status(200);
    });
    pm.test("user has an email field", function () {
        const body = pm.response.json();
        pm.expect(body.email).to.match(/@/);
    });
  2. Create an environment called jsonplaceholder-env Add one variable: baseUrl = https://jsonplaceholder.typicode.com. Use {{baseUrl}} in each request URL (e.g. {{baseUrl}}/posts/1).
  3. Export the collection and environment In Postman, right-click the collection → ExportCollection v2.1 → save as jsonplaceholder.postman_collection.json inside newman-lab/. Do the same for the environment (Environments tab → ... menu → Export).
  4. Or: skip Postman and paste these files directly If you just want to verify Newman works, write the two files below into newman-lab/. They are valid Postman v2.1 exports.

jsonplaceholder.postman_collection.json:

jsonplaceholder.postman_collection.json
{
  "info": {
    "name": "jsonplaceholder",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "GET /posts/1",
      "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/posts/1", "host": ["{{baseUrl}}"], "path": ["posts","1"] } },
      "event": [
        { "listen": "test", "script": { "type": "text/javascript", "exec": [
          "pm.test(\"status is 200\", function () { pm.response.to.have.status(200); });",
          "pm.test(\"response has userId\", function () {",
          "  const body = pm.response.json();",
          "  pm.expect(body).to.have.property(\"userId\");",
          "  pm.expect(body.id).to.eql(1);",
          "});"
        ]}}
      ]
    },
    {
      "name": "GET /posts",
      "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/posts", "host": ["{{baseUrl}}"], "path": ["posts"] } },
      "event": [
        { "listen": "test", "script": { "type": "text/javascript", "exec": [
          "pm.test(\"status is 200\", function () { pm.response.to.have.status(200); });",
          "pm.test(\"returns an array of at least 100 items\", function () {",
          "  const body = pm.response.json();",
          "  pm.expect(body).to.be.an(\"array\");",
          "  pm.expect(body.length).to.be.at.least(100);",
          "});"
        ]}}
      ]
    },
    {
      "name": "POST /posts",
      "request": {
        "method": "POST",
        "header": [ { "key": "Content-Type", "value": "application/json" } ],
        "body": { "mode": "raw", "raw": "{\"title\":\"resync-lab\",\"body\":\"hello\",\"userId\":1}" },
        "url": { "raw": "{{baseUrl}}/posts", "host": ["{{baseUrl}}"], "path": ["posts"] }
      },
      "event": [
        { "listen": "test", "script": { "type": "text/javascript", "exec": [
          "pm.test(\"status is 201\", function () { pm.response.to.have.status(201); });",
          "pm.test(\"echoes back the payload\", function () {",
          "  const body = pm.response.json();",
          "  pm.expect(body.title).to.eql(\"resync-lab\");",
          "  pm.expect(body.userId).to.eql(1);",
          "});"
        ]}}
      ]
    },
    {
      "name": "GET /users/1",
      "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/users/1", "host": ["{{baseUrl}}"], "path": ["users","1"] } },
      "event": [
        { "listen": "test", "script": { "type": "text/javascript", "exec": [
          "pm.test(\"status is 200\", function () { pm.response.to.have.status(200); });",
          "pm.test(\"user has an email field\", function () {",
          "  const body = pm.response.json();",
          "  pm.expect(body.email).to.match(/@/);",
          "});"
        ]}}
      ]
    }
  ]
}

jsonplaceholder.postman_environment.json:

jsonplaceholder.postman_environment.json
{
  "name": "jsonplaceholder-env",
  "values": [
    { "key": "baseUrl", "value": "https://jsonplaceholder.typicode.com", "enabled": true, "type": "default" }
  ],
  "_postman_variable_scope": "environment"
}

5 Run & verify

  1. Run the collection with Newman From inside newman-lab/:
    All platforms
    newman run jsonplaceholder.postman_collection.json \
      -e jsonplaceholder.postman_environment.json \
      -r cli,junitfull \
      --reporter-junitfull-export reports/junit.xml
    On Windows PowerShell, replace the trailing \ with a backtick (`) or put the whole command on one line.
  2. Expected output Newman prints a table in your terminal:
    CLI output (abridged)
    → GET /posts/1   [200 OK, 288B, 412ms]
      ✓ status is 200
      ✓ response has userId
    
    → GET /posts     [200 OK, 27.5KB, 298ms]
      ✓ status is 200
      ✓ returns an array of at least 100 items
    
    → POST /posts    [201 Created, 180B, 310ms]
      ✓ status is 201
      ✓ echoes back the payload
    
    → GET /users/1   [200 OK, 505B, 241ms]
      ✓ status is 200
      ✓ user has an email field
    
    � executed  |  4 requests, 8 assertions, 0 failures
  3. Confirm the JUnit XML was written Open reports/junit.xml. You should see a <testsuites> root with one <testsuite> per request and eight <testcase> elements in total — one per assertion.
  4. Check the exit code
    PowerShell
    echo $LASTEXITCODE
    bash/zsh
    echo $?
    Zero means all assertions passed. Any non-zero value fails the CI build. This is the whole contract CI needs from Newman.

6 Read the JUnit XML

The JUnit format is what GitHub Actions, GitLab, Jenkins, and TeamCity parse to show the green/red bar and list individual failures. A minimal shape looks like this:

reports/junit.xml (abridged)
<testsuites name="jsonplaceholder" tests="8" failures="0" time="1.261">
  <testsuite name="GET /posts/1" tests="2" failures="0">
    <testcase name="status is 200" time="0.412"/>
    <testcase name="response has userId" time="0.003"/>
  </testsuite>
  <testsuite name="POST /posts" tests="2" failures="0">
    <testcase name="status is 201" time="0.310"/>
    <testcase name="echoes back the payload" time="0.004"/>
  </testsuite>
</testsuites>

When an assertion fails, Newman adds a <failure> child with the message and stack trace. That is the text your CI surfaces on the build page, so assertion names matter — "status is 200" is a better failure message than "test 1".

CI-ready pattern: In GitHub Actions, add uses: mikepenz/action-junit-report@v4 after the Newman step and point it at reports/junit.xml. The action renders a pass/fail summary on the PR. In GitLab, add artifacts: reports: junit: reports/junit.xml to the job. Same XML, free plumbing.

7 Troubleshooting

error: unable to load reporter "junitfull"

The reporter package is not installed globally. Run npm install -g newman-reporter-junitfull. If you installed it in a local node_modules/ instead, Newman can't see it on the global PATH. Either install globally, or prefix your command with npx and install both packages as devDependencies: npm install --save-dev newman newman-reporter-junitfull and run npx newman run ....

All requests fail with ECONNREFUSED or ETIMEDOUT

Your machine cannot reach jsonplaceholder.typicode.com. Test from a browser first. If you're behind a corporate proxy, pass it explicitly: newman run ... --proxy http://your-proxy:port. If the browser works but Newman doesn't, check your firewall — some security tools block Node.js outbound calls until you approve the binary.

Tests pass in Postman but fail in Newman

Two usual suspects: (1) the environment wasn't exported or wasn't passed with -e, so {{baseUrl}} resolved to an empty string — every URL becomes /posts/1 with no host and fails. Always pass -e. (2) the collection was exported as v2.0 instead of v2.1. Re-export as Collection v2.1 from Postman's export dialog.

JUnit XML file is empty or missing

You used the wrong reporter flag. The correct pair is -r cli,junitfull and --reporter-junitfull-export reports/junit.xml. Note the reporter name and the flag prefix must match exactly: junitfull, not junit, and the export flag is --reporter-junitfull-export. If you used the built-in junit reporter, the flag is --reporter-junit-export.

8 Challenge

Make it CI-ready

Challenge 1 — chain requests with extracted variables. Add a fifth request that creates a post with POST /posts, writes the returned id to an environment variable via pm.environment.set("postId", pm.response.json().id) inside the Tests tab, then a sixth request that does GET /posts/{{postId}}. Re-run with Newman and verify the JUnit XML now has twelve test cases. Variable chaining is the single most common CI-collection pattern and the fastest way to prove you can script real workflows, not just smoke requests.

Challenge 2 — swap the target to https://reqres.in. Change baseUrl to https://reqres.in/api, update paths to /users/2 and /users, and keep the same assertion style. Re-export and re-run. What had to change in the collection? In the environment? In the Newman command? The answer — "just the environment" — is the argument you'll make to your team when proposing Newman in CI: you can target staging, UAT, and production with the same collection and different environment files.