DEV Community

Cover image for The Best Engineers I Know Don't Write Unit Tests
Tarek Mostafa
Tarek Mostafa

Posted on

The Best Engineers I Know Don't Write Unit Tests

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)

Collapse
 
maxpaint profile image
Maksym Danylenko •

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.

Collapse
 
tarikmostafa profile image
Tarek Mostafa •

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 our catch block 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:

  • Does the client driver leak an unclosed socket?
  • Does the connection pool get starved under concurrent load?
  • Does an upstream retry storm turn that single timeout into a cascading outage? You can test error branching with mocks, but you can’t test systemic behavior. That’s why I argue that testing against real testcontainers or ephemeral dependencies provides an order of magnitude more confidence than mocking out the world—even for failure cases. Really appreciate the thoughtful comment and the heart!
Collapse
 
maxpaint profile image
Maksym Danylenko •

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.

Thread Thread
 
tarikmostafa profile image
Tarek Mostafa •

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:

  • What you’re describing is ex-post verification: production taught the team a harsh lesson about driver/pool mechanics, and you codified that lesson.
  • What my article attacks is ex-ante delusion: teams writing 200 synthetic mocks before deployment, convincing themselves and their managers that 94% coverage means the distributed system is safe from day one. And you hit a profound truth: some emergent distributed behaviors (cascading retry storms, pool starvation under massive concurrency) simply cannot be captured in a simple unit test. That’s why resilience has to shift to runtime architecture (circuit breakers, backpressure, rate-limiters) and observability. Quick side note: if you guys haven't tried pairing Testcontainers with Toxiproxy (Shopify’s proxy for simulating network jitter, latency, and socket cutoffs), it’s an absolute game-changer for reproducing those exact driver timeouts and connection pool hangs locally without waiting for production to burn first. Appreciate the brilliant exchange, man. This is what engineering discussions should look like.
Thread Thread
 
tarikmostafa profile image
Tarek Mostafa •

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!