The critique of over-mocking in unit tests is worth taking seriously. It’s a real problem, and it’s more widespread than the industry generally acknowledges.
When you mock every dependency in a unit test, you’re no longer testing whether your code works – you’re testing whether your code does what you told your mocks to pretend the dependencies do. If the mocks don’t reflect production behavior, your tests pass and your production system still breaks. Worse, those tests make your codebase rigid. Every internal refactor requires updating a web of mock expectations, which creates friction, which leads to tests being deleted rather than maintained, which defeats the purpose entirely.
So far, so valid. The problem is the conclusion: that the solution is to move to integration tests instead.
Integration tests have real value, particularly for testing behavior that spans system boundaries – API contracts, database interactions, event flows. For these cases, they provide a level of confidence that no amount of mocking can replicate. That’s true.
But integration tests come with their own overhead that tends to get glossed over. They’re slower, often by an order of magnitude or more. They require running environments – databases, message queues, dependent services – that have to be provisioned, seeded, and torn down. They fail for environmental reasons unrelated to the code being tested, which erodes confidence in the test suite over time. And when they fail, the debugging surface is much larger.
Unit tests, when written well, still do things that integration tests don’t. They run in seconds rather than minutes. They force you to think about the design of your code – code that’s hard to unit test is usually telling you something about coupling. They give you precise failure information that points directly at the problem.
Martin Fowler’s test pyramid isn’t a prescription – it’s an observation that fast, focused tests should form the base of a healthy test suite because they provide the fastest feedback loop at the lowest cost. The specific proportions are contextual. But the underlying principle – that a suite dominated by slow, broad tests is harder to maintain and slower to act on – holds up.
The pragmatic approach
The goal of a test suite is confidence, not coverage numbers or ideological purity about test types.
If your unit tests are full of mocks and brittle to refactor, that’s worth fixing – but the fix is to write better unit tests, not to delete them. Test behavior, not implementation. Mock at system boundaries, not at every internal dependency. Kent Beck’s original thinking on test-driven development was about driving design decisions, not generating coverage.
For REST APIs and service boundaries, integration tests are worth investing in – and investing in the infrastructure to run them reliably. A flaky integration test that passes intermittently is often worse than no test at all because it teaches your team to ignore failures.
Most codebases benefit from both, in proportion to their context. A greenfield service might lean on integration tests early. A complex business logic layer might be better served by thorough unit tests. The nuance is the point.
There is no test strategy that works everywhere. The teams that do this well are the ones that have an honest conversation about what they’re actually testing and why – rather than applying a rule.