gap·map

Backend testing

[ middle depth ]

Where a backend test belongs

Pick the level where the bug can actually live. Logic fully determined by its inputs (tax rules, a state machine, a parser) is a unit test: fast, no I/O, thousands run per second. Behavior that only shows up when components talk to each other, a SQL query, a schema mapping, a transaction boundary, needs an integration test against the real dependency. A flow that only makes sense through the public entry point, like sign up then pay, is end to end.

The pyramid is a budget, not a quota: many unit tests, fewer integration, a thin cap of end to end. Invert it and you get the ice-cream cone, a fat layer of slow browser tests on a sliver of unit tests. CI takes 40 minutes, one flaky run blocks every release, and a red test does not tell you where the break is. The shape is about feedback speed and how well a failure localizes.

What to mock and what not to

Wrong "mock the third-party SDK and assert it was called." That test encodes your guess about the vendor's responses, and it stays green after the vendor quietly changes behavior under you.

Don't mock what you don't own. Wrap the external thing in an interface you define, mock that interface in unit tests, and cover the real adapter with an integration test against a sandbox or a container. The same rule applies to your database. An in-memory substitute, H2 standing in for Postgres, is a different engine: different constraints, a different SQL dialect. Testcontainers runs the real engine in Docker per run:

const pg = await new PostgreSqlContainer("postgres:16").start();
// migrate, run the query you actually ship, tear the container down after

Now the query, the migration, the connection pool, and the constraint each get exercised for real.

For two services that deploy independently, a contract test (consumer-driven, like Pact) records the request and response the consumer relies on, and the provider verifies against that recorded pact. Each side runs alone, so you get boundary confidence without standing up both systems. It proves shape compatibility, not a live end-to-end run.

Why your test is flaky

Flaky means the same commit passes one run and fails the next with no code change. Usual causes: async timing, shared mutable state between tests, order dependency, a real clock or timezone. The fix is determinism. A retry lifts the green rate and buries a real race.

Reset or rebuild state per test so nothing leaks across the boundary:

beforeEach(() => { db.users.length = 0; }); // fresh store, no cross-test bleed

Inject the clock instead of calling Date.now, seed any randomness, and run the suite in random order in CI so an order dependency fails the first time rather than the day you reshuffle the files.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

more Infrastructure & Ops

was this useful?