Testing Setup for Node.js with Jest and Supertest

The Ultimate Testing Setup for Node.js + TypeScript (Jest + Supertest)

If you’re building APIs with Node.js and TypeScript, testing is what gives you confidence to refactor and ship faster. In this guide, I’ll show you a practical setup using Jest, TypeScript, and Supertest—from unit tests to real HTTP integration tests.

🚀 Complete JavaScript Guide (Beginner + Advanced)

🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)


1. Project Setup

Initialize your project:

npm init -y
npm install express
npm install -D typescript ts-node @types/node
npm install -D jest ts-jest @types/jest

Create a tsconfig.json:

{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Initialize Jest:

npx ts-jest config:init

jest.config.mjs:

export default {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/src"]
};

Add scripts to package.json:

export default {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/src"]
};

2. Example Utility + Unit Test

src/utils.ts

export function add(a: number, b: number) {
  return a + b;
}

export function getErrorMessage(err: unknown): string {
  if (err instanceof Error) return err.message;
  if (typeof err === "string") return err;
  return "Unknown error";
}

src/tests/utils.test.ts

import { add, getErrorMessage } from "../utils";

describe("utils", () => {
  test("adds numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  test("gets error message from Error", () => {
    const err = new Error("Something went wrong");
    expect(getErrorMessage(err)).toBe("Something went wrong");
  });

  test("gets error message from string", () => {
    expect(getErrorMessage("Oops")).toBe("Oops");
  });
});

Run tests:

npm test

3. Testing a Class with Logic

src/task.ts

export class Task {
  name: string;
  completedAt: Date | null = null;

  constructor(name: string) {
    this.name = name;
  }

  markComplete() {
    if (this.completedAt) {
      throw new Error("Already completed");
    }
    this.completedAt = new Date();
  }
}

src/tests/task.test.ts

import { Task } from "../task";

describe("Task", () => {
  test("marks task complete", () => {
    const task = new Task("Test");
    task.markComplete();
    expect(task.completedAt).toBeInstanceOf(Date);
  });

  test("throws if already completed", () => {
    const task = new Task("Test");
    task.markComplete();
    expect(() => task.markComplete()).toThrow();
  });
});

4. Controller Test with Mocks

src/controller.ts

import { Request, Response } from "express";

export class TaskService {
  async create(name: string) {
    return { id: 1, name };
  }
}

export async function createTask(req: Request, res: Response) {
  const service = new TaskService();
  const task = await service.create(req.body.name);
  res.status(201).json({ task });
}

src/tests/controller.test.ts

import { createTask, TaskService } from "../controller";

jest.mock("../controller", () => {
  const original = jest.requireActual("../controller");
  return {
    ...original,
    TaskService: jest.fn().mockImplementation(() => ({
      create: jest.fn().mockResolvedValue({ id: 1, name: "Test" })
    }))
  };
});

test("creates task", async () => {
  const req: any = { body: { name: "Test" } };
  const json = jest.fn();
  const status = jest.fn(() => ({ json }));

  const res: any = { status };

  await createTask(req, res);

  expect(status).toHaveBeenCalledWith(201);
  expect(json).toHaveBeenCalledWith({
    task: { id: 1, name: "Test" }
  });
});

5. Integration Tests with Supertest

Install:

npm install -D supertest @types/supertest

src/server.ts

import express from "express";
import { createTask } from "./controller";

export function createServer() {
  const app = express();
  app.use(express.json());

  app.get("/health", (_, res) => {
    res.json({ ok: true });
  });

  app.post("/tasks", createTask);

  return app;
}

src/tests/integration.test.ts

import request from "supertest";
import { createServer } from "../server";

describe("API", () => {
  test("health check", async () => {
    const app = createServer();
    const res = await request(app).get("/health");

    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(true);
  });

  test("create task", async () => {
    const app = createServer();

    const res = await request(app)
      .post("/tasks")
      .send({ name: "New task" });

    expect(res.status).toBe(201);
    expect(res.body.task.name).toBe("New task");
  });
});

6. Coverage

npm run test:coverage

Aim for strong coverage on logic-heavy code. Don’t chase 100%—focus on meaningful tests.


Final Thoughts

A solid testing strategy includes:

  • Unit tests for logic
  • Mocked collaboration tests
  • Integration tests for routes

This combination gives you a codebase you can refactor without fear.

If you’re building Node.js APIs with TypeScript, investing in testing early will save you countless hours later.

Share this article

Similar Posts