Node.js API Testing
API testing sends real requests to your routes and checks the responses. These tests confirm that each address returns the right data and status code. Supertest makes this job simple, and Jest runs the checks.
The Test Drive Idea
A car maker test-drives every car before delivery. The driver presses pedals and watches the results. An API test presses your routes and watches the replies.
Test code --> request --> [ Your Express app ] --> response --> Check result (Supertest) status, body, headers
Why Test the API
- Catch broken routes before visitors do.
- Change code with confidence.
- Document how each route behaves.
- Run checks automatically on every update.
Install the Tools
npm install jest supertest --save-devAdd a test script to package.json.
"scripts": {
"test": "jest"
}Split the App from the Server
Supertest starts the app on its own. Export the app and keep listen in a separate file.
// app.js
const express = require("express");
const app = express();
app.use(express.json());
const items = [{ id: 1, name: "Pen" }];
app.get("/items", (req, res) => res.json(items));
app.get("/items/:id", (req, res) => {
const item = items.find((i) => i.id === Number(req.params.id));
if (!item) return res.status(404).json({ error: "Not found" });
res.json(item);
});
app.post("/items", (req, res) => {
if (!req.body.name) return res.status(400).json({ error: "Name required" });
const item = { id: items.length + 1, name: req.body.name };
items.push(item);
res.status(201).json(item);
});
module.exports = app;
// server.js
const app = require("./app");
app.listen(3000);Write the First Test
// app.test.js
const request = require("supertest");
const app = require("./app");
describe("GET /items", () => {
it("returns a list of items", async () => {
const res = await request(app).get("/items");
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body[0]).toHaveProperty("name", "Pen");
});
});Run the tests with npm test.
Parts of a Test
- describe groups related tests.
- it names one expected behavior.
- request(app) sends a call to your app.
- expect compares the real result with the expected result.
Test Missing Data
it("returns 404 for an unknown id", async () => {
const res = await request(app).get("/items/999");
expect(res.statusCode).toBe(404);
expect(res.body.error).toBe("Not found");
});Test Creating Data
describe("POST /items", () => {
it("creates an item", async () => {
const res = await request(app)
.post("/items")
.send({ name: "Eraser" });
expect(res.statusCode).toBe(201);
expect(res.body.name).toBe("Eraser");
});
it("rejects a missing name", async () => {
const res = await request(app).post("/items").send({});
expect(res.statusCode).toBe(400);
});
});Test both the happy path and the failure path. Failures often hide bugs.
Test Protected Routes
Send a token in the Authorization header.
it("blocks requests without a token", async () => {
const res = await request(app).get("/profile");
expect(res.statusCode).toBe(401);
});
it("allows requests with a token", async () => {
const res = await request(app)
.get("/profile")
.set("Authorization", "Bearer " + validToken);
expect(res.statusCode).toBe(200);
});Prepare and Clean Up
Hooks run code before and after tests.
beforeAll(async () => { /* connect to a test database */ });
afterEach(async () => { /* clear test data */ });
afterAll(async () => { /* close the connection */ });Use a separate test database. Never run tests against production data.
Test Layers
/\
/ \ End-to-end tests (few, slow)
/----\
/ \ API tests (some)
/--------\
/ \ Unit tests (many, fast)
/____________\
Good Habits
- Give each test one clear purpose.
- Keep tests independent. One test must not rely on another.
- Name tests with plain sentences.
- Run tests before each code commit.
Practice Tasks
- Write tests for a GET route that returns a list and a single item.
- Add a test that sends invalid data and expects status 400.
- Run Jest with --coverage and read the report.
