Build the mental model
Every test case you ran by hand in Postman follows the same shape: send a request, then check whether what came back matches what you expected. Automated testing takes that exact shape and lets a script run it, so the same checks happen every time without anyone remembering to click Send.
A test runner sends a request, then checks the response against a set of expectations. The obvious check is the status code, but a status code alone rarely proves correctness — the runner should also check body content and response headers.
Schema checks catch a hidden bug class
Confirming not just that fields are present, but that they have the right types, catches an endpoint that still returns 200 but has quietly started sending broken data — something a status-code-only check completely misses.
None of this requires a specific framework. JavaScript and Python both have popular libraries for this job, but the concept — send, then check status, body, headers, schema — transfers to whichever tool a team uses. The real shift is that tests run unattended in CI, not in a browser tab.
AUTOMATED TEST PIPELINE
-----------------------
TEST RUNNER
|
v
SEND REQUEST
|
v
CHECK RESPONSE
+-- STATUS (200? 404? matches expectation?)
+-- BODY (right fields, right values?)
+-- HEADERS (content-type, rate-limit, etc.)
+-- SCHEMA (right fields AND right types?)
|
v
PASS / FAIL REPORT (with a reason for every failure)Connect it to a real scenario
Say you have three requests you'd normally re-run by hand after every deploy: get a user, get a missing user, and create an order. Automating them means writing down what each should return once, then letting a runner check all three every time.
The code below pairs each request with an expected status and a list of required fields. The runner sends the request, compares status, and checks every expected field exists — collecting specific failure reasons instead of a vague pass or fail.
Real intentional bug included
Running the example surfaces a real bug: the order-creation endpoint is missing a "total" field its own test expects, so that test fails with a precise reason while the other two pass.
Try the working example
// Mock responder standing in for a real HTTP call.
function mockApi(request) {
if (request.method === "GET" && request.path === "/users/1") {
return {
status: 200,
headers: { "content-type": "application/json" },
body: { id: 1, name: "Aye Aye", email: "aye@example.com" },
};
}
if (request.method === "GET" && request.path === "/users/999") {
return { status: 404, headers: {}, body: { error: "not found" } };
}
if (request.method === "POST" && request.path === "/orders") {
// Bug on purpose: forgets to include "total" in the response.
return {
status: 201,
headers: { "content-type": "application/json" },
body: { id: 55, status: "created" },
};
}
return { status: 500, headers: {}, body: {} };
}
// A minimal automated test runner: checks status, body fields, and
// header presence for a batch of test definitions against a responder.
function runAutomatedTests(testDefs, responder) {
return testDefs.map((def) => {
const response = responder(def.request);
const failures = [];
if (response.status !== def.expectedStatus) {
failures.push(
`expected status ${def.expectedStatus}, got ${response.status}`
);
}
for (const field of def.expectedFields || []) {
if (!(field in (response.body || {}))) {
failures.push(`missing expected field "${field}" in response body`);
}
}
if (def.expectedHeader && !(def.expectedHeader in response.headers)) {
failures.push(`missing expected header "${def.expectedHeader}"`);
}
return {
name: def.name,
passed: failures.length === 0,
failures,
};
});
}
const testDefs = [
{
name: "GET /users/1 returns a full user schema",
request: { method: "GET", path: "/users/1" },
expectedStatus: 200,
expectedFields: ["id", "name", "email"],
expectedHeader: "content-type",
},
{
name: "GET /users/999 returns 404 for a missing user",
request: { method: "GET", path: "/users/999" },
expectedStatus: 404,
expectedFields: ["error"],
},
{
name: "POST /orders returns the created order with its total",
request: { method: "POST", path: "/orders" },
expectedStatus: 201,
expectedFields: ["id", "status", "total"],
},
];
const report = runAutomatedTests(testDefs, mockApi);
let passCount = 0;
report.forEach((r) => {
console.log(`[${r.passed ? "PASS" : "FAIL"}] ${r.name}`);
r.failures.forEach((f) => console.log(` - ${f}`));
if (r.passed) passCount++;
});
console.log(`${passCount}/${report.length} tests passed`);Actual output when run:
[PASS] GET /users/1 returns a full user schema
[PASS] GET /users/999 returns 404 for a missing user
[FAIL] POST /orders returns the created order with its total
- missing expected field "total" in response body
2/3 tests passed
The failure is exact and actionable: it names the missing field instead of just saying "test failed," which is what makes automated checks worth building.5-minute try-it
Add a fourth test definition to the code above for an endpoint of your choosing (real or invented) that checks status, at least two expected fields, and one expected header. Run it and confirm the report explains exactly why it passes or fails.
One important caution
Checking only the status code in an automated test and skipping body/schema checks — the exact gap that let the missing "total" field slip through.
Writing failure messages that just say "test failed" instead of naming what was expected versus what was found, making failures slow to diagnose.
Martin Fowler — The Practical Test Pyramid — API Integration & Webhooks