Most of the test suites I’ve inherited share a pattern: heavy unit tests, anemic integration tests, and an embarrassing number of bugs that ship despite green CI.
What mocks actually buy you
Mocks make tests fast and isolated. Those are good properties — but they’re not the same as correct. A mocked database call doesn’t care if your query is wrong. A mocked HTTP client doesn’t notice when the API contract changes.
The bugs that escape to production almost never live inside a single unit. They live at the seams.
What integration tests buy you
An integration test that spins up a real Postgres, runs the same migrations production runs, and exercises a real HTTP handler will catch:
- Wrong SQL
- Missing indexes (well — performance, not correctness, but you’ll feel them)
- Auth bugs at the middleware boundary
- Migrations that work on an empty DB but fail on real data
None of those are things a unit test would find.
The investment
The usual objection is "integration tests are slow and flaky." Both are solvable:
- Use a fast database driver and a per-test transaction that rolls back. Sub-second setup.
- Run tests in parallel against separate schemas. Most ORMs support this.
- Use a real container in CI (
postgres:16-alpineboots in 2s).
A one-time investment of a couple of days, and the suite goes from "flaky and slow" to "reliable and fast enough."
My current default
A pyramid that’s been inverted: 70% integration tests, 20% unit tests for genuinely complex logic, 10% end-to-end. The bug-escape rate dropped, the suite stayed fast enough, and I stopped having to write vi.mock(...) calls I’d immediately forget about.