Integration Testing with a Test DB
Unit tests with mocks verify that individual functions behave correctly in isolation, but they cannot verify that your route handlers, middleware, database queries, and business logic all work correctly together. Integration tests do this by running your actual Express app against a real database instance. The standard pattern in Node.js is to use mongodb-memory-server to spin up an in-process MongoDB instance for each test suite, or use a separate test database with Postgres/MySQL. This lesson covers the full setup: in-memory MongoDB, test lifecycle hooks, and writing integration tests that go all the way from HTTP request to database read.
In-Memory MongoDB with mongodb-memory-server
mongodb-memory-server downloads and runs a real MongoDB binary in process memory. Your Mongoose models connect to it exactly as they would connect to a real MongoDB instance. At the end of the test run, the binary is stopped and all data is discarded — no cleanup needed, no shared state between test runs.
npm install --save-dev jest supertest mongodb-memory-server// tests/setup.js — run before all test suites
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
// Clear all collections between tests to prevent data contamination
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
// jest.config.js
module.exports = {
testEnvironment: 'node',
globalSetup: './tests/globalSetup.js', // optional: download mongo binary once
setupFilesAfterFramework: ['./tests/setup.js']
};Writing Integration Tests
Integration tests look like unit tests from the outside: they use Jest and Supertest. The difference is that no database operations are mocked — the test sends a real HTTP request, the route handler runs, the Mongoose model queries the in-memory database, and the test asserts on the real response. This catches bugs that unit tests miss: wrong field names in queries, missing database indexes causing timeouts, and Mongoose validation errors that your route handler does not handle correctly.
// tests/integration/products.test.js
const request = require('supertest');
const app = require('../../app');
const Product = require('../../models/Product');
// setup.js runs before this file via setupFilesAfterFramework
describe('POST /api/products', () => {
it('creates a product in the database and returns 201', async () => {
const res = await request(app)
.post('/api/products')
.send({ name: 'Cricket Bat', price: 120, category: 'equipment' });
expect(res.statusCode).toBe(201);
expect(res.body.name).toBe('Cricket Bat');
expect(res.body.id).toBeDefined();
// Verify it was actually saved to the database
const saved = await Product.findById(res.body.id);
expect(saved).not.toBeNull();
expect(saved.name).toBe('Cricket Bat');
expect(saved.price).toBe(120);
});
it('returns 422 when required fields are missing', async () => {
const res = await request(app)
.post('/api/products')
.send({ price: 120 }); // missing name
expect(res.statusCode).toBe(422);
expect(res.body.error).toMatch(/name/i);
});
});
describe('GET /api/products', () => {
beforeEach(async () => {
await Product.insertMany([
{ name: 'Bat', price: 120, category: 'equipment' },
{ name: 'Helmet', price: 85, category: 'equipment' },
{ name: 'Ball', price: 15, category: 'equipment' }
]);
});
it('returns all products with pagination metadata', async () => {
const res = await request(app)
.get('/api/products?page=1&limit=2');
expect(res.statusCode).toBe(200);
expect(res.body.data).toHaveLength(2);
expect(res.body.meta.total).toBe(3);
expect(res.body.meta.pages).toBe(2);
});
it('filters by category', async () => {
const res = await request(app)
.get('/api/products?category=equipment');
expect(res.statusCode).toBe(200);
expect(res.body.data.every(p => p.category === 'equipment')).toBe(true);
});
});