100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js & Express Backend
30 minintermediate

Integration Testing with a Test DB

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.

Analogy🏏Cricket
Think of it like cricket: Unit tests are like a batter practising in the nets against a bowling machine — perfectly controlled, isolated, reproducible. Integration tests are like a full team practice session — real bowlers, real fielders, real conditions. The batting machine can't replicate Jasprit Bumrah's late swing or MS Dhoni's unpredictable field placement. Only the full practice session reveals how the batting technique holds up against real opposition. Your integration tests reveal how your route handlers hold up against a real database, real middleware execution, and real response serialisation.

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.

bash
npm install --save-dev jest supertest mongodb-memory-server
javascript
// 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']
};
Analogy🏏Cricket
Think of it like cricket: The in-memory MongoDB is the portable practice pitch that the team carries on tour. It sets up in minutes, provides real playing conditions, and is packed away cleanly after each session. afterEach that clears all collections is like re-rolling the pitch between sessions: each team starts with a clean, unaffected surface. No one inherits divots from the previous session.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: an integration test looks identical to a unit test from the boundary — same Jest, same Supertest, same crowd view — but it's a full-strength practice match, not a bowling-machine net. Nothing is mocked: a real HTTP request goes in, the route handler actually runs, the Mongoose model genuinely queries the in-memory database, and you assert on the real response, the way a proper trial match puts batsmen against live bowlers, real fielders, and an actual scorer instead of a machine. That's exactly why it catches bugs the net session can't — a wrong field name in a query or a missing index is like a miscommunication between batsman and runner that only shows up when real players are on the pitch together, never in solo drills. The payoff: exercising the true request-to-database path end to end surfaces the wiring bugs — bad query fields, missing indexes, broken handoffs between layers — that isolated unit tests, by design, can never see.
javascript
// 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);
  });
});
Lesson 32 of 36
0% complete