SaaS product engineering from Lahore+92 300 477 4507LinkedInWe are hiring

Quality · 2 min read

Why our integration tests start a real MongoDB or PostgreSQL in a container instead of mocking the driver.

A mocked database driver tells you that your code called the driver. It does not tell you that the query is correct, that the index exists, or that a unique constraint fires when two requests race. Those are the bugs that reach production.

For integration tests we start the real database in a throwaway container, run the suite against it, and throw it away. Testcontainers makes that a few lines in the test setup.

The setup

import { MongoDBContainer } from '@testcontainers/mongodb';

let mongo: StartedMongoDBContainer;

beforeAll(async () => {
  mongo = await new MongoDBContainer('mongo:7').start();
  process.env.MONGO_URL = mongo.getConnectionString() + '?directConnection=true';
}, 60_000);

afterAll(() => mongo.stop());

The same pattern works for PostgreSQL and Redis. The container is pinned to the version production runs, so a test passes or fails for the same reasons the live system would.

What it catches that mocks miss

  • Queries that are valid code but wrong data: a missing filter on a tenant id, a sort on an unindexed field.
  • Constraint behaviour: unique indexes, cascade rules, transaction rollbacks.
  • Migrations: the suite runs against the schema the migration produces, not the one a developer remembers.
  • Serialization: dates, decimals and ObjectIds as the driver really returns them.

Keeping it fast

Start one container per test file, not per test, and reset state between tests by dropping collections or truncating tables. Unit tests still mock at the boundary; only the integration layer pays the container cost, and CI caches the images.

The result is a suite people trust. When it is green, the database layer works, not just the code that calls it.

TechSea engineering team