Postman and Newman

Postman for API exploration and collection building; Newman for running those collections in CI.

Postman for API exploration and collection building; Newman for running those collections in CI.


Collection Structure

A Postman Collection is a structured set of API requests.

Good collection structure:
  Collection: Order API
    Folder: Auth
      POST /auth/login                   (pre-request: clear old token)
      POST /auth/refresh
    Folder: Orders — Happy Path
      POST /api/orders                   (test: status 201, save order_id)
      GET  /api/orders/{{order_id}}      (test: status 200, status=pending)
      PUT  /api/orders/{{order_id}}/confirm
      GET  /api/orders/{{order_id}}      (test: status=confirmed)
    Folder: Orders — Error Cases
      POST /api/orders (missing quantity) (test: status 400, error message)
      POST /api/orders (invalid product)  (test: status 422)
    Folder: Teardown
      DELETE /api/orders/{{order_id}}    (cleanup — run after tests)

Collection Variables and Environments

// Environment: staging.json
{
  "name": "Staging",
  "values": [
    {"key": "base_url", "value": "https://api-staging.example.com", "enabled": true},
    {"key": "api_version", "value": "v1", "enabled": true},
    {"key": "auth_token", "value": "", "enabled": true}
  ]
}

// Environment: production.json
{
  "name": "Production",
  "values": [
    {"key": "base_url", "value": "https://api.example.com", "enabled": true},
    {"key": "api_version", "value": "v1", "enabled": true},
    {"key": "auth_token", "value": "", "enabled": true}
  ]
}

Pre-request Scripts

// Pre-request script on the collection root — runs before every request
// Auto-refresh the auth token when it's missing or expired

const tokenExpiry = pm.collectionVariables.get("token_expiry");
const now = Date.now();

if (!tokenExpiry || now > parseInt(tokenExpiry)) {
    const loginRequest = {
        url: pm.environment.get("base_url") + "/auth/login",
        method: "POST",
        header: { "Content-Type": "application/json" },
        body: {
            mode: "raw",
            raw: JSON.stringify({
                username: pm.environment.get("username"),
                password: pm.environment.get("password"),
            }),
        },
    };

    pm.sendRequest(loginRequest, (err, response) => {
        if (err) throw new Error("Login failed: " + err);
        const body = response.json();
        pm.environment.set("auth_token", body.access_token);
        pm.collectionVariables.set("token_expiry", Date.now() + 55 * 60 * 1000); // 55 min
    });
}

Test Scripts

// Tests tab on POST /api/orders

// 1. Status code
pm.test("Status is 201 Created", () => {
    pm.response.to.have.status(201);
});

// 2. Response schema
pm.test("Response has order id and status", () => {
    const body = pm.response.json();
    pm.expect(body).to.have.property("id");
    pm.expect(body).to.have.property("status");
    pm.expect(body.status).to.equal("pending");
});

// 3. Save ID for subsequent requests in the same run
pm.test("Save order_id for chained requests", () => {
    const body = pm.response.json();
    pm.collectionVariables.set("order_id", body.id);
});

// 4. Latency SLA
pm.test("Response time < 500ms", () => {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

// 5. Header assertions
pm.test("Response has Content-Type application/json", () => {
    pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});

Newman — CLI Execution

# Install
npm install -g newman

# Run a collection against staging environment
newman run collections/order-api.postman_collection.json \
    --environment environments/staging.json \
    --reporters cli,junit \
    --reporter-junit-export results/newman-results.xml \
    --bail                          # stop on first failure
    --timeout-request 10000         # 10s per request timeout
    --delay-request 200             # 200ms between requests (rate limiting)

# Run specific folder only
newman run collections/order-api.postman_collection.json \
    --environment environments/staging.json \
    --folder "Orders — Happy Path"

# With environment variable overrides (useful for CI secrets)
newman run collections/order-api.postman_collection.json \
    --environment environments/staging.json \
    --env-var "password=$API_PASSWORD" \
    --env-var "username=ci-test-user"

CI Integration

# .github/workflows/api-tests.yml
name: API Integration Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 */4 * * *"  # every 4 hours against production (smoke)

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Newman
        run: npm install -g newman newman-reporter-htmlextra

      - name: Run API tests  staging
        env:
          API_PASSWORD: ${{ secrets.STAGING_API_PASSWORD }}
        run: |
          newman run collections/order-api.postman_collection.json \
            --environment environments/staging.json \
            --env-var "password=$API_PASSWORD" \
            --reporters cli,junit,htmlextra \
            --reporter-junit-export results/junit.xml \
            --reporter-htmlextra-export results/report.html

      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Newman API Tests
          path: results/junit.xml
          reporter: java-junit

      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: newman-report
          path: results/report.html

Converting to Automated Tests

# Postman collections are great for exploration, not long-term regression.
# Once a scenario is stable, convert it to pytest + httpx for better maintainability.

# Postman test (brittle, JSON-based, no version control diff)
# Newman run → pass/fail → done

# Converted pytest test (version controlled, composable, type-checked)
import httpx
import pytest

@pytest.mark.asyncio
async def test_create_order_returns_201(api_client: httpx.AsyncClient, auth_headers):
    response = await api_client.post(
        "/api/orders",
        json={"product_id": "prod_abc", "quantity": 1},
        headers=auth_headers,
    )
    assert response.status_code == 201
    body = response.json()
    assert "id" in body
    assert body["status"] == "pending"

# Use Postman/Newman for:
#   - Exploratory API testing
#   - Ad-hoc environment checks
#   - Sharing with developers who don't write Python
#   - Smoke tests that non-engineers can run

# Use pytest + httpx for:
#   - Permanent regression tests
#   - Tests that need complex logic (retries, data setup, parametrize)
#   - CI integration where test quality matters

Common Failure Cases

Collection variable order_id not set when a chained request runs Why: the pm.collectionVariables.set("order_id", ...) call in request A's test script runs asynchronously; request B in the same run fires before A's script completes and receives undefined. Detect: request B returns a 404 or validation error referencing {{order_id}} being literal text or empty. Fix: ensure pm.collectionVariables.set is called synchronously inside pm.test, and add a null-check test that fails early if the variable is not set before request B executes.

Pre-request auth script silently fails, sending requests without a token Why: the pm.sendRequest callback receives an error but the script does not throw or fail the request, so subsequent requests use a blank auth_token and all return 401. Detect: every request in the run returns 401; the Newman output shows no auth-related failure in the pre-request phase. Fix: add if (err) { throw new Error("Login failed: " + err); } in the pm.sendRequest callback, which causes Newman to mark the pre-request as failed and halt the run.

Newman --bail stops the run before teardown deletes test data Why: --bail exits on the first test failure, skipping the teardown folder that deletes created resources, leaving orphaned records in the test database. Detect: the test database accumulates duplicate test orders or users after failed CI runs. Fix: use Newman's --folder flag to run teardown explicitly in a separate Newman invocation that runs unconditionally as a CI step after the main run, regardless of exit code.

Environment JSON committed with real credentials Why: developers export environments from Postman including populated secret values and commit the JSON to version control. Detect: the environment file in the repo contains non-empty auth_token, password, or API key values. Fix: export environments with all secret values cleared (empty string), use --env-var "password=$SECRET" in the Newman CLI to inject values from CI secrets at runtime.

Connections

technical-qa/tqa-hub · technical-qa/api-testing · technical-qa/api-performance-testing · technical-qa/api-contract-testing · qa/test-reporting · technical-qa/test-reporting-dashboards

Open Questions

  • What is the most common failure mode when implementing this at scale?
  • How does this testing approach need to adapt for distributed or microservice architectures?