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)
- 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.
-
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
-
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
-
Verify Newman is on your PATH
All platformsExpected output: a version number like
newman --version
6.2.1. If the command is not found on macOS or Linux, add$(npm config get prefix)/binto yourPATH.
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
-
Create a project folder
All platforms
mkdir newman-lab cd newman-lab mkdir reports
-
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.
-
In Postman, create a new collection called
jsonplaceholderAdd four requests, one per row below. For each, open the Tests tab and paste the matching snippet.Postman Tests tab — GET /posts/1pm.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 /postspm.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 /postspm.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/1pm.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(/@/); }); -
Create an environment called
jsonplaceholder-envAdd one variable:baseUrl=https://jsonplaceholder.typicode.com. Use{{baseUrl}}in each request URL (e.g.{{baseUrl}}/posts/1). -
Export the collection and environment
In Postman, right-click the collection → Export → Collection v2.1 → save as
jsonplaceholder.postman_collection.jsoninsidenewman-lab/. Do the same for the environment (Environments tab → ... menu → Export). -
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:
{
"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:
{
"name": "jsonplaceholder-env",
"values": [
{ "key": "baseUrl", "value": "https://jsonplaceholder.typicode.com", "enabled": true, "type": "default" }
],
"_postman_variable_scope": "environment"
}5 Run & verify
-
Run the collection with Newman
From inside
newman-lab/:All platformsOn Windows PowerShell, replace the trailingnewman run jsonplaceholder.postman_collection.json \ -e jsonplaceholder.postman_environment.json \ -r cli,junitfull \ --reporter-junitfull-export reports/junit.xml
\with a backtick (`) or put the whole command on one line. -
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
-
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. -
Check the exit code
PowerShell
echo $LASTEXITCODE
Zero means all assertions passed. Any non-zero value fails the CI build. This is the whole contract CI needs from Newman.bash/zshecho $?
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:
<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".
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.