This episode tests APIs with modern tools: the built-in node:test test runner, HTTP endpoint testing with supertest, mocking for isolation, and coverage measurement. You lock down the API behavior so changes don't break features.

Untested code is a time bomb: one small change can break an unexpected feature. Testing changes that — you write expectations as code, then run them every time the application changes. In the Node.js world, testing tools are getting simpler and simpler.
Episode 18 puts modern API testing into practice: the built-in node:test test runner, HTTP endpoint testing with supertest, mocking to isolate the database, and coverage measurement. By the end of the episode, you'll have a test suite that runs in the terminal and is ready for CI.
Since Node.js version 18, a test runner is available without installing anything. The node:test module provides test and assert:
import { test } from "node:test";
import assert from "node:assert/strict";
function tambah(a, b) {
return a + b;
}
test("tambah menjumlahkan dua angka", () => {
assert.equal(tambah(2, 3), 5);
});test("name", () => ...) defines a test case, and assert.equal(tambah(2, 3), 5) checks the result. Run it with node --test: the runner finds test files, runs them, and reports the results.
node --testnode --test finds files matching patterns like *.test.js and runs them. Add "test": "node --test" to the scripts in package.json so the whole team uses the same command — later in episode 22, this command will be used by the CI pipeline.
supertest lets you test an HTTP server without opening it to a port — requests are sent to the server function directly:
npm install --save-dev supertestnpm install --save-dev supertest adds supertest to devDependencies because it's only used during development and testing.
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { app } from "./app.js";
test("GET /api/pengguna returns a list", async () => {
const respons = await request(app).get("/api/pengguna");
assert.equal(respons.status, 200);
assert.equal(respons.body.status, "success");
assert.ok(Array.isArray(respons.body.data));
});request(app).get("/api/pengguna") sends a request to the Express application without starting a server. Notice that app must be exported from a separate module, and app.listen moves to another file — this pattern makes the application easy to test. The assertions check the status code and body shape.
Tests that depend on a real database are slow and brittle. Use mocking to replace external parts with behavior you control. The simplest way: accept dependencies as parameters:
export function buatHandler(database) {
return async (req, res) => {
const data = await database.ambilSemua();
res.json(data);
};
}With the dependency injection pattern above, tests can pass in a fake database. database.ambilSemua() is implemented as a function returning fake data — with no real connection. node:test also provides mock.fn and mock.method to create these function replacements.
Don't just test the happy path. Also test error responses so the application behaves correctly when something fails:
test("404 when the user doesn't exist", async () => {
const respons = await request(app).get("/api/pengguna/999");
assert.equal(respons.status, 404);
assert.equal(respons.body.status, "error");
});The test above ensures a user with a non-existent id gets a 404. Well-tested error cases prevent bugs that only appear under certain conditions — and give you confidence when refactoring.
Coverage shows the percentage of code executed by tests. Node.js has built-in coverage via V8:
node --test --experimental-test-coveragenode --test --experimental-test-coverage reports the lines, functions, and branches covered by tests. A reasonable target isn't 100 percent, but stable, targeted coverage of important code — high coverage doesn't guarantee correct code, it only signals what's untested.
Tests that don't run automatically are just a script. In episode 22, we'll connect npm test to the CI pipeline so every push runs the whole suite — a change that breaks tests immediately fails the build.
Here's what to take away:
node:test provides a built-in runner and assertions without installing anything.app from listen so it's easy to test.In the next episode, episode 19, we'll discuss performance tuning, the event loop, and profiling — the event loop phases, causes of blocking, CPU profiling with the Node Inspector, load testing with autocannon, and memory leak detection. You'll build an API that's fast and stable under load.