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:
| Double | Purpose |
|---|---|
| Stub | Returns canned data for calls (e.g. a fake findById) |
| Mock | A stub that also asserts it was called correctly |
| Spy | Wraps a real function to record calls, optionally overriding it |
| Fake | A 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
undefinedfrom every method until you script them. A test that suddenly fails with “Cannot read properties of undefined” usually means you forgot amockResolvedValuefor 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
| Need | Use |
|---|---|
| Test service/controller logic only | jest.mock the repository |
| Stop real calls to a third-party API | nock interception |
| Verify queries, schema, or indexes | In-memory database |
| Observe a real call without replacing it | jest.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-serverinstance per suite (beforeAll/afterAll) and clear collections per test — restartingmongodfor every test is slow. - Use
mockResolvedValue/mockRejectedValueto model both the success and failure paths of every dependency.