DevPath · Learn to code ESPTEN

Testing and advanced Node

Backend testing

Why test the backend

An automated test is code that runs your code and fails if the behavior is not the expected one. On the backend it protects us from breaking endpoints, business rules or data contracts when refactoring.

Two big families stand out:

Isolate with test doubles (mocks)

To test a unit without its real dependencies you use doubles: fake objects that replace a dependency (the database, an external service). A mock or spy also records how it was called, so you can assert about it afterwards.

// Service that depends on a "repository" (data access).
function registerUser(repo, name) {
  return repo.save({ name, created: true });
}

// In the test, we pass it a FAKE repo that spies on the calls.
const calls = [];
const repoMock = {
  save(user) { calls.push(user); return { id: 1, ...user }; },
};

const r = registerUser(repoMock, "Ana");
// We verify behavior (not internal implementation):
//   - save was called exactly once
//   - with the correct name
//   - and we returned what the repo returned

The key to the mock: check that it was called correctly (how many times, with which arguments) without running the real dependency.

Test endpoints with supertest

For integration tests of an HTTP server, the supertest library launches the app on an ephemeral port and lets you write assertions about the response:

import request from "supertest";
import { app } from "./app.js";

await request(app)
  .get("/users/1")
  .expect(200)
  .expect((res) => {
    if (res.body.name !== "Ana") throw new Error("incorrect name");
  });

supertest takes care of starting and closing the server; you only describe the request (.get, .post, .send(...)) and what you expect (.expect(...)).

TDD (Test-Driven Development)

Test-driven development reverses the usual order into a short cycle:

  1. Red: first write a test that describes the desired behavior. It fails (the code does not exist yet).
  2. Green: write the minimum code to make the test pass.
  3. Refactor: clean up the code with the safety net of the green test.

TDD pushes you to design small, testable functions, and leaves a test suite as a natural byproduct.

Examples

A simple spy: record the calls in an array

function notify(service, message) {
  service.send(message);
}

const calls = [];
const serviceMock = { send: (m) => calls.push(m) };

notify(serviceMock, "Hello");
notify(serviceMock, "Goodbye");

console.log(calls.length);   // 2 -> it was called twice
console.log(calls[0]);       // "Hello" -> with the correct argument
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
Advanced asynchrony and streams →