Skip to main content

Command Palette

Search for a command to run...

The Code Coverage Illusion: When Unit Tests Create False Confidence

Updated
5 min readView as Markdown
The Code Coverage Illusion: When Unit Tests Create False Confidence
K
Software Architect focused on software development, architecture, engineering processes, and team leadership. I write about practical architecture decisions, system design, distributed systems, database engineering, backend development, and the real tradeoffs teams face while building and maintaining software systems.

High code coverage may look impressive, but it does not necessarily mean that an application has been tested well.

Over the years, I have repeatedly seen teams invest significant developer time in writing unit tests and achieving high code coverage.

However, many of these tests, especially mock-heavy tests, provide little practical value. They are often written to satisfy a metric (code coverage) rather than reduce meaningful business risk

The Code Coverage Trap

Management often treats 70% or higher code coverage as proof of software quality. Developers are then expected to write enough unit tests to reach that target.

The problem is simple: code coverage tells us which code was executed, not whether the right behaviour was tested.

A test can execute a method, assert a predefined result and increase the coverage percentage without proving that the application works correctly.

Developers spend time writing and maintaining these tests, management sees a green dashboard, and everyone assumes that a best practice has been followed.

In reality, the organisation may simply be wasting time, effort and money.

When the Real Logic Lives in the Database

Many enterprise applications are still primarily CRUD-based app. In several legacy systems, most business rules are buried inside large stored procedures rather than implemented in a strong business or domain layer. The application layer is often limited to thin controllers, services and data-access wrappers.

Unit tests normally isolate external dependencies. Database calls are mocked, and predefined results are returned to the business layer. But when the actual business logic lives inside stored procedures, mocking the database removes the core logic that needs to be tested.

The test passes. Coverage increases. But the stored procedure, database mapping and actual business rules remain untested.

This creates the appearance of confidence, not real confidence.

When Tests Preserve Existing Bugs

Another common problem is writing unit tests around existing production code without independently verifying whether that code is correct.

Developers often treat the current implementation as the source of truth. They observe what a method currently returns and write a test that expects the same result. If the production code contains a defect, the test simply documents and protects that defective behaviour.

Tests should be based on expected business rules, not blindly derived from the existing implementation. Anyone writing and reviewing them must understand the business requirement, expected input and output, boundary conditions and failure scenarios.

If production code contradicts a valid business rule, the test should fail and expose the defect. It should not be adjusted merely to make the existing implementation pass.

Otherwise, the test suite becomes a mirror of potentially defective code. You get coverage, but no value.

When Integration Tests Are Better

When most business behaviour depends on stored procedures, databases or external APIs, integration tests usually provide greater value.

Unlike mock-heavy unit tests, integration tests can execute real database objects, verify mappings, call external services and validate actual workflows. They may run more slowly, but they test the parts where the real risk exists!

In many cases, a slower integration test that validates a critical business workflow provides far more value than a fast, mock-heavy unit test that proves very little.

Test for Confidence, Not Coverage

The answer is not to abandon unit testing or replace every test with an end-to-end test. That would create a different set of problems, including slow execution, difficult diagnosis and fragile test environments.

The right approach is to choose the test type according to the architecture and risk of the application.

Code coverage can be a useful indicator, but it should never become the definition of quality!

Where Unit Tests Actually Help

Unit testing is valuable when the application contains meaningful logic that can be evaluated independently, such as:

  • complex business calculations;

  • pricing, eligibility or validation rules;

  • algorithms and parsers;

  • state transitions;

  • important boundary conditions;

  • and logic with clearly verifiable expected results.

Teams should identify business-critical functionality and write focused unit tests around it. Testing every thin controller, pass-through service or repository wrapper simply to improve coverage is pointless.

The goal is NOT to maximise the number of tests or the coverage percentage.

The goal is to gain the greatest possible confidence and business value from the time and money invested in testing

A Better Testing Strategy

Choose tests based on risk and architecture:

Unit tests for isolated logic, integration tests for external dependencies and end-to-end tests for critical business journeys.

Situation Most useful approach
Complex, isolated business logic Unit tests
Rich Domain Models Unit tests
Having anti patterns like Anemic Domain models Integration tests
Stored-procedure-driven business rules Integration tests
Interaction between services or components Integration or contract tests
Critical user or business workflows End-to-end tests
Previously reported production defect Focused regression test at the appropriate level
Thin controller or pass-through wrapper Often no dedicated test unless it adds meaningful risk

Before writing a test, ask:

  1. What business risk does this test protect against?

  2. Is the expected result derived from a real requirement?

  3. Am I testing meaningful behaviour or merely implementation details?

  4. Does mocking remove the very dependency where the real risk exists?

  5. Would an integration test provide substantially greater confidence?

  6. Will this test help detect a real regression, or is it being written only to increase coverage?

If the team cannot answer these questions, the test probably does not deserve to exist.

Unlearning Myths: Teams, Technology and Trade-offs

Part 1 of 2

A real-world look at the beliefs, habits, and practices software teams rarely question. Lessons drawn from two decades of building systems, leading teams, and discovering what actually works.

Up next

Why Great Managers Never Chased the Title

The strongest managers are not always the people who chased the title. Often, they are the people who cared deeply enough about the work to accept responsibility for leading it. Steve Jobs had a blunt