Skip to content
Express.js ex testing 5 min read

Mocking Databases & Services

Real databases and third-party APIs make tests slow, flaky, and dependent on network access — exactly the qualities you want to avoid in a fast feedback loop. Mocking replaces those collaborators with controllable stand-ins so each test runs deterministically and offline. This page covers the spectrum of test doubles: mocking modules with Jest, intercepting outbound HTTP with nock, and running real query logic against an in-memory database with mongodb-memory-server. Knowing which technique to reach for is the difference between a brittle suite and a trustworthy one.

Test doubles, briefly

“Test double” is the umbrella term for any object that stands in for a real dependency. The common kinds you will use in Express tests are:

DoublePurpose
StubReturns canned data for calls (e.g. a fake findById)
MockA stub that also asserts it was called correctly
SpyWraps a real function to record calls, optionally overriding it
FakeA working but lightweight implementation (e.g. an in-memory DB)

Jest blurs these lines — a jest.fn() is both stub and mock — but the mental model still helps you decide how much realism a given test needs.

Mocking a database module

The simplest and most common case is mocking the module that talks to your database, so the controller under test never opens a connection. jest.mock('../path') auto-replaces every export with a mock function; you then script the return values per test.

// src/repositories/userRepo.js
const { pool } = require('../db');

async function findById(id) {
  const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
  return rows[0] || null;
}

async function create(user) {
  const { rows } = await pool.query(
    'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *',
    [user.name, user.email]
  );
  return rows[0];
}

module.exports = { findById, create };
// src/services/userService.test.js
const userRepo = require('../repositories/userRepo');
const userService = require('./userService');

jest.mock('../repositories/userRepo'); // auto-mock all exports

describe('userService.register', () => {
  afterEach(() => jest.clearAllMocks());

  test('rejects a duplicate email', async () => {
    userRepo.findById.mockResolvedValue({ id: '1', email: '[email protected]' });

    await expect(userService.register({ id: '1' })).rejects.toThrow(
      'already exists'
    );
    expect(userRepo.create).not.toHaveBeenCalled();
  });

  test('persists a new user', async () => {
    userRepo.findById.mockResolvedValue(null);
    userRepo.create.mockResolvedValue({ id: '2', name: 'Ada' });

    const result = await userService.register({ name: 'Ada' });

    expect(userRepo.create).toHaveBeenCalledWith({ name: 'Ada' });
    expect(result).toEqual({ id: '2', name: 'Ada' });
  });
});

Because the repository is mocked, no SQL ever runs — the test exercises only the service’s decision logic, which is what a unit test should do.

Auto-mocked modules return undefined from every method until you script them. A test that suddenly fails with “Cannot read properties of undefined” usually means you forgot a mockResolvedValue for a call the code makes.

Intercepting outbound HTTP with nock

When a route calls an external API (a payment gateway, a geocoder), you do not want real network traffic in tests. nock intercepts Node’s HTTP layer and answers matching requests with a canned response — no change to your application code required.

npm install --save-dev nock
// src/services/weather.js
const axios = require('axios');

async function getTemp(city) {
  const { data } = await axios.get('https://api.weather.test/v1/current', {
    params: { city },
  });
  return data.tempC;
}

module.exports = { getTemp };
// src/services/weather.test.js
const nock = require('nock');
const { getTemp } = require('./weather');

afterEach(() => nock.cleanAll());

test('returns the temperature for a city', async () => {
  nock('https://api.weather.test')
    .get('/v1/current')
    .query({ city: 'Oslo' })
    .reply(200, { tempC: 4 });

  await expect(getTemp('Oslo')).resolves.toBe(4);
});

test('propagates a 503 from the upstream API', async () => {
  nock('https://api.weather.test')
    .get('/v1/current')
    .query(true)
    .reply(503, { error: 'unavailable' });

  await expect(getTemp('Oslo')).rejects.toThrow('503');
});

To guarantee no real requests slip through, disable live connections globally in your test setup:

// jest.setup.js
const nock = require('nock');
beforeAll(() => nock.disableNetConnect());
afterAll(() => nock.enableNetConnect());

Output:

PASS  src/services/weather.test.js
  ✓ returns the temperature for a city (8 ms)
  ✓ propagates a 503 from the upstream API (3 ms)

In-memory databases for higher fidelity

Module mocks verify logic but not your actual queries. When you want to test the queries themselves — schema, indexes, validation — run them against a real database engine started in memory. mongodb-memory-server downloads a mongod binary and spins up a throwaway instance per test run, giving full Mongo behaviour with no external service.

npm install --save-dev mongodb-memory-server
// src/models/user.test.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const User = require('./user');

let mongod;

beforeAll(async () => {
  mongod = await MongoMemoryServer.create();
  await mongoose.connect(mongod.getUri());
});

afterEach(async () => {
  await User.deleteMany({}); // isolate each test
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongod.stop();
});

test('enforces the unique email index', async () => {
  await User.init(); // ensure indexes are built
  await User.create({ name: 'Ada', email: '[email protected]' });

  await expect(
    User.create({ name: 'Ada2', email: '[email protected]' })
  ).rejects.toThrow(/duplicate key/);
});

This sits between a unit and an integration test: real persistence, but isolated and disposable. Reach for it when the value being tested lives in the database layer rather than in JavaScript.

Choosing the right approach

NeedUse
Test service/controller logic onlyjest.mock the repository
Stop real calls to a third-party APInock interception
Verify queries, schema, or indexesIn-memory database
Observe a real call without replacing itjest.spyOn

Best Practices

  • Mock at the boundary you own — the repository or HTTP client — not deep internals, so refactors do not break every test.
  • Reset doubles between tests (jest.clearAllMocks(), nock.cleanAll(), deleteMany) to prevent state leaking across cases.
  • Call nock.disableNetConnect() in setup so an unmocked request fails loudly instead of hitting the real internet.
  • Prefer an in-memory database over hand-mocking when the query itself is the thing under test.
  • Assert on how a mock was called (toHaveBeenCalledWith), not just on the final result, to catch wrong arguments.
  • Keep one mongodb-memory-server instance per suite (beforeAll/afterAll) and clear collections per test — restarting mongod for every test is slow.
  • Use mockResolvedValue/mockRejectedValue to model both the success and failure paths of every dependency.
Last updated June 14, 2026
Was this helpful?