Last year, a critical payment processing service went completely silent during Black Friday traffic.
Transactions stalled. Errors spiked. The team scrambled.
The craziest part?
Code coverage was 94%. Every single CI unit test pipeline was glowing green.
The database timeout was mocked. The third-party payment gateway was mocked. The Redis distributed lock was mocked. The pipeline happily verified that our imaginary world of fake dependencies worked in complete harmony.
Meanwhile, in the cold, unyielding reality of production, the system was dead on arrival.
Over the last decade working across high-scale distributed systems, I noticed a pattern that feels like heresy to say out loud in modern agile culture:
The most effective, battle-tested software engineers I know almost never write traditional unit tests.
Here is why—and what they build instead.
1. The Mocking Industrial Complex
Somewhere around 2014, the software industry conflated testing with mocking.
Today, a typical "unit test" in enterprise software looks like this:
- 40 lines of setup configuring mocks, stubs, and synthetic responses.
- 3 lines invoking the actual method.
- 15 lines asserting that
mockService.call()was called exactly once with specific parameters. Ask yourself: What did this test actually prove? It proved that your code calls the mock the way you told the mock to expect to be called. That’s not a test; that’s circular reasoning disguised as quality assurance. Even worse, these tests couple directly to implementation details, not behavior. The moment an engineer attempts to refactor internal logic without changing external contracts, 18 unit tests explode with red errors. Unit tests don't make code safe to refactor. In most codebases, they freeze bad architecture in place.
2. Testing Your Assumptions Against Your Own Assumptions
The fatal flaw of the isolated unit test is simple:
A unit test cannot verify reality. It can only verify your imagination of reality.
If you believe Postgres behaves in a certain way when a unique constraint collides during a concurrent transaction, you configure your mock to mirror that belief.
If your belief is wrong, your unit test passes, your CI merges the PR, and production crashes at 2:00 AM.
Real production bugs in modern software rarely happen because a simple calculation was wrong. They happen at the boundaries:
- Network timeouts and retry storms.
- Race conditions between distributed workers.
- Serialization mismatches between microservices.
- Database locking behaviors under high concurrency. Unit tests, by definition, eliminate the boundaries. They test the logic in vacuum, precisely where it is least likely to fail in catastrophic ways.
3. Code Coverage is a Vanity Metric That Breeds Cynicism
When management mandates "85% Unit Test Coverage," they think they are buying reliability.
What they are actually buying is test theater.
Developers are smart creatures who respond to incentives. When you mandate an arbitrary percentage, engineers stop thinking about risk and start thinking about lines executed. They write tests for getters, boilerplate mappers, and trivial orchestrators.
They write tests with zero meaningful assertions just to turn lines green in SonarQube.
It eats up 30% of engineering bandwidth, inflates CI run times, and gives the business a false sense of security.
4. What Elite Engineers Do Instead
If they aren't writing unit tests, are they just cowboy coding directly to main?
Absolutely not. The best engineers are obsessed with correctness—they just place their bets where the return on investment (ROI) is exponentially higher.
Here is what replaces the unit test frenzy:
A. Real Boundary Verification (Testcontainers & Ephemeral Envs)
Instead of mocking the database or Kafka, they spin up a lightweight, real instance using tools like Testcontainers.
If a test passes, they know with 100% certainty that the actual SQL migration, indices, and driver semantics work against an actual database engine. One real integration test is worth fifty mocked unit tests.
B. Making Illegal States Unrepresentable
Instead of writing 15 unit tests checking for null, invalid negative values, or malformed states, they encode those invariants directly into the type system and domain value objects.
If the compiler won't allow an invalid order state to exist, you don't need a unit test to verify what happens when it does.
C. Contract Testing Over Synthetic Mocks
When services talk to each other, they don't mock the downstream API. They use tools like Pact or schema registry validation to enforce strict, bi-directional contracts. If an upstream service changes a JSON key, the contract test fails before deployment.
D. Production Resilience & Observability
They understand that no test suite captures the chaos of real human users. So they invest the time they saved from writing trivial mocks into:
- Fine-grained structured logging and distributed tracing.
- Automated canary rollouts and instant rollback triggers.
- Circuit breakers and graceful degradation fallbacks. If your system can automatically isolate a failing microservice without taking down the checkout flow, you don't need to panic about whether every internal helper function had 100% test coverage.
The Uncomfortable Truth
Unit tests are fantastic for isolated algorithms: cryptographic functions, regex parsers, financial amortization calculations, and pure mathematical operations.
If you are writing pure logic, write unit tests.
But 90% of modern software engineering is not algorithmic; it is plumbing and integration. It is orchestrating databases, cloud services, external APIs, and state transitions.
Mocking the world to claim you tested the plumbing is just professional delusion.
Stop measuring how many lines of code you tested in a vacuum. Start measuring how fast your system recovers when the real world refuses to behave like your mocks.
What’s your stance?
Do you still enforce 80%+ unit test coverage on your team, or have you shifted your focus to integration and production observability? Let’s debate in the comments.
Top comments (5)
The problem here isn't unit tests, it's the test cases. Every mock returned success, so the suite only checked the happy path. The cases that mattered were never written: the DB times out, the gateway doesn't answer, the lock isn't acquired. A mock is great for exactly that, making a dependency fail on purpose and checking what your code does next. 94% coverage with none of those cases just means the happy path ran 94% of the lines.
Spot-on point about happy-path bias, Maksym. You hit the nail on the head: a 94% coverage metric usually just means 94% happy-path compliance. Teams chase the percentage, and green happy paths are the fastest way to get there.
Where I think mocks still fundamentally deceive us—even in negative testing—is the fidelity of the failure:
When we mock a database failure, we usually mock a clean
TimeoutException. The test verifies ourcatchblock logs an error and returns HTTP 503. Great.What the mock doesn't simulate is what a real distributed timeout does to the surrounding plumbing:
Agreed, and for the database specifically I'm fully with you: test the real behavior with Testcontainers. We have skills for that side too, they're just in a private repo for now.
Where I'd push back a bit is real failures. They're exactly what's hard to reproduce with Testcontainers or similar tools. Socket leaks, pool starvation and retry storms under real load rarely show up in an ephemeral container. Before the incident, the best we can do is read the docs on how the driver and pool behave on timeout, and think through the failure modes.
The real knowledge usually comes from prod. When something breaks there, we write a test together with the fix. It still uses a mock, but the mock reproduces the actual failure we saw, not the clean TimeoutException you described. That's the test that stops the incident from coming back.
This is the most nuanced and practical counter-argument on this entire thread, Maksym. 100% respect for this workflow.
You are describing the gold standard of post-incident maturity: taking an actual, bloody production failure and pinning it down with a regression test so it never bites you twice. When a mock is calibrated against an observed, real-world post-mortem trace, it actually carries high fidelity.
The crucial distinction, I think, is timing:
By the way, Maksym—your pragmatic mindset around real-world failure modes is exactly the kind of engineering ethos that led me to write my book, The Unshakeable Developer.
I’d love to gift you a Kindle copy simply as a thank-you for this high-signal discussion—strictly no strings attached. If you're interested, let me know what email or link works best to send it your way!