Why API testing needs its own layer
Unit tests check a function in isolation. End-to-end UI tests check the whole product through a browser. Neither of those is great at answering the question a backend team actually cares about day to day: "did this endpoint just break its contract?" — wrong status code, missing field, broken auth, a response shape that changed without anyone updating the docs. That's the gap API testing frameworks fill, and it's exactly the space covered in "Top 10 API Testing Tools" on Medium, which lists tools like Postman, SoapUI, and Rest-Assured side by side.
This article focuses on one of the most widely adopted combinations in that space: Postman for designing and exploring requests, and Newman, Postman's official CLI runner, for turning that collection into something a pipeline can execute automatically.
What Newman actually is
Newman is the command-line collection runner published and maintained by the Postman team. It takes a Postman collection — a JSON file with requests, assertions written in JavaScript, and environment variables — and runs it outside the Postman desktop app, which is what makes it usable in CI/CD.
Official repository: https://github.com/postmanlabs/newman
Target API for this example
Instead of a private backend, I used httpbin.org, a small, public HTTP testing service that echoes back whatever you send it — ideal for demonstrating real requests without needing any authentication or infrastructure of my own.
Step 1: writing assertions inside a Postman request
Inside the Postman app, each request has a "Tests" tab where you write plain JavaScript assertions that run against the response:
// Tests tab for GET https://httpbin.org/get?user=marymar
pm.test("Status code is 200", () => {
pm.response.to.have.status(200);
});
pm.test("Response contains the query param sent", () => {
const body = pm.response.json();
pm.expect(body.args.user).to.eql("marymar");
});
pm.test("Response time is under 800ms", () => {
pm.expect(pm.response.responseTime).to.be.below(800);
});
For a POST request that sends a JSON body:
// Tests tab for POST https://httpbin.org/post
pm.test("Status code is 200", () => {
pm.response.to.have.status(200);
});
pm.test("Echoed body matches what was sent", () => {
const body = pm.response.json();
pm.expect(body.json.title).to.eql(pm.collectionVariables.get("title"));
});
Step 2: exporting the collection
From Postman: collection menu → Export → Collection v2.1 → save as api-tests.postman_collection.json. That file is what Newman consumes — it's plain JSON, so it belongs in version control right next to the application code it's testing.
Step 3: running it with Newman
npm install -g newman
newman run api-tests.postman_collection.json \
--env-var baseUrl="https://httpbin.org" \
--reporters cli,html \
--reporter-html-export reports/report.html
Newman prints a summary per request — assertions passed, failed, response time — and, with the html reporter, generates a shareable report that doesn't require anyone to have Postman installed to read it.
Step 4: wiring it into GitHub Actions
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
newman:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Newman
run: npm install -g newman newman-reporter-html
- name: Run API tests
run: |
newman run api-tests.postman_collection.json \
--env-var baseUrl="https://httpbin.org" \
--reporters cli,html,junit \
--reporter-html-export reports/report.html \
--reporter-junit-export reports/report.xml
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: newman-report
path: reports/
The --reporters cli,html,junit line matters: cli for readable logs in the Actions output, html for a report a non-technical teammate can open, and junit because most CI dashboards (GitHub, GitLab, Jenkins) know how to render JUnit XML as a native test summary with pass/fail counts.
A more complete real-world example
For a fuller example that organizes tests by feature (auth, user profile, and a resource-tracking flow) instead of one flat collection, this project walks through the same Postman + Newman + GitHub Actions pattern end to end:
https://github.com/postmanlabs/newman (the runner itself — clone it to see its own test suite structure as a reference for organizing a Newman-driven project)
Where this approach falls short
Newman runs exactly what's in the exported JSON — it has no concept of the API's actual behavior beyond the assertions someone wrote by hand. It won't generate edge cases for you, and a collection that's gone stale (testing an old response shape) will happily keep "passing" against a mismatched mental model of the API. It's a contract-verification tool, not a fuzzer or a schema-validation engine — for deeper coverage, pairing it with a schema validation step (e.g. validating the response against an OpenAPI spec) closes that gap.
Conclusion
Postman gets teams to write and explore requests quickly; Newman is what turns that exploration into a repeatable, CI-friendly safety net. The combination doesn't require learning a new language or framework — if a team can already write a Postman request, they can write a Postman test, and from there the jump to "this runs on every pull request" is just a few lines of YAML.
Top comments (0)