Backend testing
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.