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:
- 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
Unit Testing: Services
Services contain the business logic — test them thoroughly. They take plain inputs and return plain outputs. No HTTP to set up.
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:
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.
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:
Tests import app directly — no port, no network, no listen().
Test Helpers
Utilities that every test file needs:
Mocking Strategies
Mock at the right level. Mock the layer just below the layer under test:
Never mock things two layers away — that defeats the point of the test.
Module mocks with jest.mock:
Resetting mock state:
Spying on a function without replacing it:
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:
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:
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
What Not to Test
Some things are expensive to test and rarely break:
- External libraries — don't test that
bcrypt.hashworks. Trust the library. - Express internals — don't test that
router.getregisters routes correctly. - Simple getters/setters —
user.name = 'Jatin'doesn't need a test. - Trivially thin code — a repository function that calls
prisma.user.findUniqueand 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.tsfromindex.ts— tests import the app without starting a listener. jest.mock()replaces entire modules;jest.spyOn()intercepts specific functions while leaving others intact. Alwaysjest.clearAllMocks()inbeforeEach.- 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 --noEmitbefore 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.
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 & RegisterDiscussion
0Join the discussion