Node.js integration testing with Jest and Supertest

Integration Testing Node.js APIs with Jest and Supertest

Writing reliable and maintainable tests is a must when building APIs with Node.js. In this guide, you’ll learn how to write integration tests for Node.js and Express APIs using Jest and Supertest, with practical TypeScript examples you can apply immediately. While unit tests should make up the majority of your test suite, integration tests play a crucial role—especially when it comes to testing middleware, request validation, and error handling.

In this post, we’ll walk through how to write integration tests for a Node.js Express API using Jest and Supertest, based on a real-world project setup.

🚀 Complete JavaScript Guide (Beginner + Advanced)

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


Why Integration Tests Matter

Integration tests exercise multiple parts of your application together:

  • Routing
  • Middleware (auth, validation, error handling)
  • Controllers
  • Use cases

Middleware in particular is difficult to test with unit tests alone. Integration tests allow you to send real HTTP requests into your API and assert against the responses—just like a real client would.


Installing Supertest

Supertest makes it easy to test HTTP endpoints without running a real server.

npm install supertest @types/supertest --save-dev

We’ll assume Jest is already set up in your project.


Providing an Express App to Supertest

Supertest needs an Express app instance. In this project, server.ts exposes a createServer function that returns an Express app:

// server.ts
import express from "express";

export function createServer() {
  const app = express();

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

  return app;
}

This setup makes testing much easier.


Writing Your First Integration Test

Create a test file:

src/tests/integrationTests.test.ts

Then write a simple test for the /health endpoint:

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

describe("Health endpoint tests", () => {
  it("Health endpoint returns ok 200", async () => {
    const app = createServer();

    const response = await request(app).get("/health");

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

Run Jest in watch mode:

npm run test -- --watch

If this passes, your integration testing setup is working.


Testing a Real Endpoint: Create Task

Now let’s test a more realistic scenario: creating a task via POST /v1/tasks.

The controller uses a CreateTaskUseCase class. We don’t want to hit the database during tests, so we’ll mock this dependency.


Mocking the Use Case

import request from "supertest";
import { createServer } from "../server";
import { CreateTaskUseCase } from "../routes/v1/tasks/createTask.usecase";

jest.mock("../routes/v1/tasks/createTask.usecase");

describe("Create task integration tests", () => {
  const executeMock = jest.fn();

  beforeEach(() => {
    (CreateTaskUseCase as jest.Mock).mockImplementation(() => ({
      execute: executeMock,
    }));
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it("creates a new task", async () => {
    executeMock.mockResolvedValue({ id: 1, name: "New task" });

    const app = createServer();

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

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

Bypassing Authentication Middleware

If your route is protected by authentication middleware, you may see a 401 Unauthorized error.

For integration tests, you can mock the middleware to simply pass through:

jest.mock("../middleware/authenticate-user", () => ({
  __esModule: true,
  default: () => (_req: any, _res: any, next: any) => next(),
}));

This keeps your tests focused on behavior, not authentication.


Testing Error Handling

Error handling often lives in middleware, which makes it ideal for integration testing.

it("returns an error when task creation fails", async () => {
  executeMock.mockRejectedValue(new Error("Error creating task"));

  const app = createServer();

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

  expect(response.status).toBe(500);
  expect(response.body).toHaveProperty("error");
});

This verifies that your global error handler works correctly.


Testing Request Validation

This project uses a validation middleware powered by Joi. The task name is required.

it("validates task payload by requiring name", async () => {
  const app = createServer();

  const response = await request(app)
    .post("/v1/tasks")
    .send({ description: "Missing name" });

  expect(response.status).toBe(422);
  expect(response.body.error.code).toBe("ERR_VALID");
});

This test exercises:

  • Validation middleware
  • Joi schema
  • Error handler

All in one request.


Measuring Test Coverage

Run the coverage report:

npm run test -- --coverage

Even a small number of integration tests can significantly increase coverage because they touch multiple modules at once.


Best Practices

  • Keep most of your tests unit tests
  • Use integration tests selectively
  • Focus integration tests on:
    • Middleware
    • Validation
    • Error handling
    • Redirect-heavy flows

Integration tests are powerful—but they should complement, not replace, unit tests.

Happy testing 🚀

Frequently Asked Questions (FAQ)

Should I use integration tests or unit tests for Node.js?

You should use both. Unit tests should make up the majority of your test suite, while integration tests are best for middleware, validation, error handling, and request/response flows.

Is Supertest good for Express APIs?

Yes. Supertest is one of the most popular libraries for testing Express APIs because it allows you to test endpoints without running a real HTTP server.

Do integration tests replace end-to-end tests?

No. Integration tests sit between unit tests and end-to-end tests. They test multiple modules together but usually mock external systems like databases and third-party services.

Share this article

Similar Posts