Module P-5·23 min read

Unit testing with Jest, integration testing with Supertest, mocking modules, database testing strategies, meaningful coverage, and CI integration.

Module P-5 — Testing Node.js Applications

What this module covers: Untested code is a liability — you cannot refactor it safely, you cannot ship it confidently, and you cannot onboard a new engineer without fear. This module covers the testing pyramid for Node.js: unit tests with Jest that test one function in isolation, integration tests with Supertest that hit real Express routes, the correct way to mock dependencies so tests stay fast and deterministic, database testing strategies, and wiring tests into CI. By the end you will have a test suite that actually catches bugs and runs in seconds.


The Testing Pyramid

Three levels of tests, each with a different trade-off:

text
  • Unit tests: Test a single function — a service method, a validator, a utility. No HTTP, no database, no network. Run in milliseconds.
  • Integration tests: Test an Express route from HTTP request to HTTP response. Real validation middleware, real service logic, but the database layer is mocked or uses a test database.
  • End-to-end tests: Spin up the full stack against a real database. Slow and brittle. Write few, keep them for critical paths only.

For a Node.js API, the sweet spot is: many unit tests for service/business logic, integration tests for every route, minimal E2E tests.


Setup: Jest and Supertest

bash
json
json

Unit Testing: Services

Services contain the business logic — test them thoroughly. They take plain inputs and return plain outputs. No HTTP to set up.

typescript

Notice the pattern: set up mocks, call the function, assert on the output or thrown error. Each test describes one behaviour. The test names read like a specification.


Unit Testing: Validators

Schemas are pure functions — trivial to test:

typescript

Integration Testing: Routes with Supertest

Integration tests send real HTTP requests to your Express app (in memory — no server port needed) and assert on the response.

typescript

Separating the App from the Server

For Supertest to work, your Express app must be importable without starting a server. Split app.ts from index.ts:

typescript
typescript

Tests import app directly — no port, no network, no listen().


Test Helpers

Utilities that every test file needs:

typescript
typescript
typescript
json

Mocking Strategies

Mock at the right level. Mock the layer just below the layer under test:

text

Never mock things two layers away — that defeats the point of the test.

Module mocks with jest.mock:

typescript

Resetting mock state:

typescript

Spying on a function without replacing it:

typescript

Database Testing Strategies

Three options, in order of preference:

Option 1: Mock repositories entirely (preferred for unit/integration tests)

Repositories are mocked in service tests. The service never touches a real database. Fast, hermetic, no setup.

Option 2: Test database with real Prisma

For testing repositories or critical data flows, use a separate test database:

bash
typescript
typescript

Option 3: In-memory database (SQLite for fast repo tests)

Use an in-memory SQLite database for fast, zero-setup repository tests. Prisma supports this with the sqlite provider.


Test Coverage: What Actually Matters

Coverage numbers are a proxy — what matters is whether your tests catch regressions. Some guidelines:

typescript

Focus coverage where bugs are expensive: services (business logic), validators (input contracts), error handling paths.

Don't chase 100%: boilerplate routes, configuration files, and type declaration files don't need tests. Use /* istanbul ignore next */ for legitimately untestable branches.


Running Tests in CI

yaml

What Not to Test

Some things are expensive to test and rarely break:

  • External libraries — don't test that bcrypt.hash works. Trust the library.
  • Express internals — don't test that router.get registers routes correctly.
  • Simple getters/settersuser.name = 'Jatin' doesn't need a test.
  • Trivially thin code — a repository function that calls prisma.user.findUnique and returns the result doesn't need a unit test; the integration test covers it.

Test the code you wrote. Mock the code others wrote.


Summary

  • Unit tests target services and validators — fast, no database, mock dependencies one layer down.
  • Integration tests target Express routes via Supertest — real validation middleware runs, services are mocked.
  • Separate app.ts from index.ts — tests import the app without starting a listener.
  • jest.mock() replaces entire modules; jest.spyOn() intercepts specific functions while leaving others intact. Always jest.clearAllMocks() in beforeEach.
  • Mock at the right level — service tests mock repositories, route tests mock services. Never skip a layer.
  • Coverage thresholds enforce a floor on test quality in CI — set them for the service layer first, where bugs are most expensive.
  • CI runs tsc --noEmit before tests — catch type errors before runtime errors.

Next: configuration, environment management, and security hardening — environment variables, secrets management, rate limiting, CORS, and the security headers that prevent the most common API vulnerabilities.


Knowledge Check

What is the recommended balance of test types for a Node.js API according to the testing pyramid?


Why should you separate your Express application (app.ts) from your server listener (index.ts)?


Which of the following is considered an anti-pattern (what NOT to test) when writing your test suite?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.