API Testing with Newman
Turning Postman collections into an automated API test gate with Newman: the anatomy of a collection, writing assertions, running one collection against all six implementations, data-driven tests, schema and contract validation, and wiring it into CI on plain Docker Engine — no new test framework to learn.
Postman is already one of the tools this book reaches for to exercise an API by hand. Newman is its command-line runner: it executes the very same Postman collection headlessly, which turns the requests your team already pokes at interactively into an automated test gate you can run on every change. The payoff is that the assertions live in one place — the collection — and the same file serves three audiences: a developer clicking through Postman, a reviewer reading the tests, and a CI pipeline failing the build when an assertion breaks.
Why Newman
A Postman collection is an executable contract, not just a saved list of requests. Newman runs it without the GUI and emits machine-readable results.
There is no new test framework to learn: the tests are the JavaScript assertions already supported inside Postman, and Newman is the bridge from “works when I click it” to “verified on every pull request.”
Anatomy of a collection
A collection nests folders and requests; each request can carry a pre-request script, the HTTP call itself, and a block of tests that run against the response. Environment variables supply the parts that change between runs.
The tests are small JavaScript blocks using Postman’s pm API. A request that creates an order
might assert the status, the response time, and the shape of the body:
{
"name": "Create order",
"request": {
"method": "POST",
"header": [{ "key": "Idempotency-Key", "value": "{{$guid}}" }],
"url": "{{base_url}}/orders",
"body": { "mode": "raw", "raw": "{\"sku\":\"A-1\",\"qty\":2}" }
},
"event": [{
"listen": "test",
"script": { "exec": [
"pm.test('201 Created', () => pm.response.to.have.status(201));",
"pm.test('under 300ms', () => pm.expect(pm.response.responseTime).to.be.below(300));",
"const body = pm.response.json();",
"pm.test('returns an id', () => pm.expect(body).to.have.property('id'));",
"pm.collectionVariables.set('order_id', body.id);"
] }
}]
}
That last line stores the new id in a variable so a later request — fetch, cancel, or pay for the order — can chain off it. Chaining requests through variables is how a collection models a whole flow, not just isolated calls.
One collection, six backends
Because Newman exercises the API as a black box over HTTP, the collection does not know or care which language implements the service. The same collection is the conformance suite for every implementation in this book.
This is a quietly powerful idea: the collection is the API specification made executable. Point it at whichever implementation is running and the assertions are identical. Bringing the service under test up is the only language-specific step; the Newman command that follows is the same.
# Spring Boot — start the service under test, then run the same collection
./mvnw spring-boot:run & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
# Quarkus — dev mode, then the same collection
./mvnw quarkus:dev & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
# .NET — run the service, then the same collection
dotnet run & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
# Python / FastAPI — serve, then the same collection
uvicorn app.main:app --port 8080 & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
# C++ / Drogon — build and run, then the same collection
cmake --build build && ./build/order-service & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
# Go — run the service, then the same collection
go run ./cmd/order-service & # http://localhost:8080
newman run orders.postman_collection.json -e local.postman_environment.json
Running Newman
The runner takes the collection and an environment file, and you choose reporters for the
audience. cli is for a human watching; junit emits the XML that CI systems turn into test
reports; htmlextra (a community reporter) produces a shareable page.
# install once (Node.js): npm install -g --prefix ~/.local newman@6.2.3 newman-reporter-htmlextra@1.23.1
newman run orders.postman_collection.json \
--environment local.postman_environment.json \
--reporters cli,junit,htmlextra \
--reporter-junit-export results/newman.xml \
--bail \ # stop on first failure (fail fast in CI)
--timeout-request 5000 # per-request ceiling
--bail stops at the first failed assertion, which is usually what you want in a gate; drop it
when you would rather see every failure in one run.
Data-driven tests
A single set of requests can be run repeatedly over a table of inputs with --iteration-data.
This is how you cover boundary values, bad input, and edge cases without duplicating requests.
newman run orders.postman_collection.json -e local.postman_environment.json \
--iteration-data cases.csv # columns become {{variables}} in the requests
# cases.csv: sku,qty,expected_status
# A-1,2,201
# A-1,0,422 # zero quantity must be rejected
# A-1,99999,422 # over stock must be rejected
The expected value travels in the data too, so a single test can assert
pm.response.to.have.status(Number(pm.iterationData.get('expected_status'))) and the one request
becomes a table of positive and negative cases.
Contract and schema validation
Beyond status codes, a test can validate the response shape against a JSON Schema — which is how
you turn the metadata and error-format work from earlier appendices into an enforced contract. A
Problem Details error body or an order representation can be checked field by field.
"pm.test('error is RFC 9457 Problem Details', () => {",
" const schema = {",
" type: 'object',",
" required: ['type', 'title', 'status'],",
" properties: {",
" type: { type: 'string' },",
" title: { type: 'string' },",
" status: { type: 'integer' }",
" }",
" };",
" pm.expect(pm.response.json()).to.be.jsonSchema(schema);",
"});"
Asserting against the schema rather than exact values keeps the test stable as data changes while still catching a contract break — a removed field, a wrong type, a renamed property — the moment it appears.
Newman in CI
The runner exits non-zero when any assertion fails, which is all a pipeline needs to gate a merge. The services come up on Docker Engine exactly as they do on a laptop — no managed cloud — and Newman runs against them.
# .github/workflows/api-tests.yml (any runner that has Docker Engine + Node works)
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d --build --wait # order, payment, inventory, …
- run: ./scripts/wait-for-ready.sh # poll /readyz before testing
- run: npm i -g newman@6.2.3 newman-reporter-htmlextra@1.23.1
- run: |
newman run orders.postman_collection.json \
-e ci.postman_environment.json \
--reporters cli,junit \
--reporter-junit-export results/newman.xml
- uses: actions/upload-artifact@v4 # keep the report even on failure
if: always()
with: { name: newman-results, path: results/ }
The readiness poll matters: start testing only once the services report ready (the readiness endpoint from the Observability and Graceful Shutdown material), or the first requests race the boot and fail spuriously.
Where Newman fits
Newman is not a replacement for unit tests — it is the layer that sits on top of them. It owns the API/contract tier: real HTTP against a running service, fast enough to run on every pull request.
Keep the base of fast unit tests broad, use Newman for the contract and integration behaviour that only shows up over real HTTP, and reserve a thin layer of full end-to-end tests for the few journeys that genuinely need the whole system. Pushing everything into Newman makes a slow, brittle suite; ignoring it leaves the contract untested.
Negative and authentication testing
The cases that matter most are the ones that are easy to skip. Assert the failure paths as
deliberately as the happy path: a 401 with a WWW-Authenticate header when the token is missing,
a 403 when it is present but unauthorised, a 409 or 422 on a business-rule violation, and a
429 carrying RateLimit headers when a client exceeds its budget. Confirm idempotency by sending
the same Idempotency-Key twice and asserting the second response matches the first rather than
creating a duplicate. These tie the testing appendix back to the error-format, security,
idempotency, and rate-limiting material — a contract is only enforced if its error behaviour is
tested, not just its success behaviour.
Take-aways and references
Treat the Postman collection as the executable specification of your API, and let Newman run it
headlessly so the same assertions guard every change. Because Newman tests over HTTP, one
collection is the conformance suite for all six implementations. Validate response shape against
a schema, not just status codes; drive edge cases from data rather than duplicated requests; and
assert the failure paths as carefully as the happy path. Wire it into CI on plain Docker Engine so the
build fails on a broken contract, and keep Newman in its lane — the API/contract tier, on top of unit
tests and under a thin end-to-end layer. The canonical references are the Postman documentation and
the Newman command-line documentation (install, reporters, and the --iteration-data and
--bail flags), and the Postman Learning Center’s guides on writing tests with the pm API and on
JSON-schema validation.
Cross-check it yourself
Prove the gate end to end against the running stack. Bring the services up with docker compose up -d --build --wait,
then newman run the orders collection with --reporters cli,junit and confirm a green run and a
newman.xml you could hand to CI. Now make the contract break on purpose — have the service return
200 instead of 201 on create, or drop the id field — and confirm Newman exits non-zero and
the failing assertion names the exact expectation. Finally, add a negative row to your
--iteration-data (a zero-quantity order expecting 422) and confirm it passes against a correct
service and fails against one that wrongly accepts it. A suite that goes red on a real contract
break, and only then, is the whole value of this appendix.
Verification status: verified — examples/28-newman/ passes 11/11 checks on Docker Engine (2026-10-10)
(a Newman collection run with status-code and state assertions (201, 422, 404, 204, cancel) and JUnit XML export).