In engineering software development, ensuring comprehensive testing is critical for reliability, safety, and regulatory compliance. Code coverage tools are essential for identifying untested paths in your codebase — the specific sequences of code that never execute during your test suite. By systematically uncovering these gaps, engineering teams can reduce hidden bugs, improve software robustness, and meet stringent industry standards such as DO-178C (avionics) or ISO 26262 (automotive). This article explores how code coverage tools work, the types of coverage they provide, and how to leverage them to find and address untested paths.

What Are Code Coverage Tools?

Code coverage tools are software utilities that monitor which parts of your source code are executed when you run your test suite. They work by instrumenting the code — inserting probes or counters — either at compile time (for compiled languages) or at runtime (for interpreted languages). After the tests run, the tool aggregates the execution data and produces a coverage report showing the percentage of code exercised, along with a detailed breakdown of which lines, branches, and paths were hit.

The primary goal is to measure test thoroughness, but coverage tools also directly highlight untested code. When a function, conditional branch, or logical path is never visited, it appears as uncovered in the report. This gives developers a precise map of testing gaps that demand attention.

Coverage tools support multiple languages and platforms. For engineering software written in C/C++, tools like gcov and BullseyeCoverage are common. For Java-based systems, JaCoCo is the de facto standard. For .NET, OpenCover or Coverlet are widely used. Regardless of the ecosystem, the principle remains the same: measure what is tested, then focus efforts on what is not.

Types of Coverage and Their Importance

Code coverage is not a single metric. Different types of coverage reveal different aspects of test completeness. For engineering software — where safety and correctness are paramount — understanding the distinction is crucial.

Line Coverage

Line coverage (also called statement coverage) measures the percentage of executable lines of code that were run during testing. It is the simplest metric and often the most widely reported. If a line is never executed, it is an obvious untested path. However, line coverage can be misleading: a test might execute every line but still miss dangerous behavior because a conditional branch was never taken.

Branch Coverage

Branch coverage measures whether every possible decision outcome (true/false for if, case for switch, loop exits) has been exercised. In C/C++ and Java, branch coverage is typically expressed as a percentage of all branches. Untested branches are direct untested paths that can hide logic errors. For example, an if-else may have the else branch uncovered, meaning the else path has never been executed in any test.

Path Coverage

Path coverage is the most comprehensive but also the hardest to achieve. It requires that every possible unique execution path through a function or module be tested. For a function with multiple nested conditions, the number of paths grows exponentially (path explosion). In practice, path coverage is often approximated by combining branch and condition coverage. Safety-critical standards like DO-178C Level A may require Modified Condition/Decision Coverage (MC/DC) as a practical substitute, where each condition within a decision is shown to independently affect the outcome.

Condition Coverage (MC/DC)

Condition coverage ensures that each Boolean sub-expression (condition) in a decision has been evaluated to both true and false. MC/DC goes further by requiring that each condition independently changes the decision's outcome. This is the most rigorous form of coverage for safety-critical engineering software and directly exposes untested paths through complex logic. For example, in an avionics flight control system, MC/DC analysis might reveal that a specific sensor fault condition never causes a system shutdown because the test suite never exercised that combination of inputs.

Understanding these coverage types allows teams to choose the right metric for their certification level and risk profile. For high-integrity systems, relying solely on line coverage is dangerous; untested branches and paths can lead to catastrophic failures.

Why Identify Untested Paths?

Untested paths represent code sequences that have never been validated. In engineering software — embedded control systems, simulation engines, or medical device firmware — these gaps can cause failures that lead to safety hazards, performance degradation, or regulatory non-compliance. Real-world examples illustrate the stakes:

  • Therac-25 (1980s): A radiation therapy machine failed due to a race condition in its control software that had never been tested under certain operational sequences. The code path that allowed the error was uncovered by testing but only after a deadly accident.
  • Mars Climate Orbiter (1999): A navigation code path that mixed metric and imperial units was never exercised in ground tests. The result was catastrophic mission loss.
  • Toyota unintended acceleration (2009): Critical code paths in the ECU were not tested under real-world conditions, leading to a recall of millions of vehicles.

Identifying untested paths before release is a proactive risk mitigation strategy. It also helps satisfy regulatory auditors: standards like ISO 26262, DO-178C, and IEC 62304 require structural coverage analysis as part of the verification process. By using coverage tools to find untested paths, teams can document compliance and build confidence in their software.

Using Coverage Tools to Find Untested Paths

The practical workflow for identifying untested paths involves several steps, beginning with instrumentation and ending with targeted test creation.

Instrumentation and Test Execution

First, compile or run your code with coverage instrumentation enabled. For gcov, compile with -fprofile-arcs -ftest-coverage. For JaCoCo, use the agent via the -javaagent flag. Run your full test suite. The tool records which code is hit and writes raw data files (e.g., .gcda for gcov, .exec for JaCoCo).

Generate and Review Coverage Reports

Use the tool's reporting command (e.g., gcov, jacoco:report) to produce HTML or XML reports. These reports color-code lines (green = hit, red = not hit) and list uncovered branches. Focus first on functions or modules with low coverage percentages. For each uncovered line or branch, ask: Is this code reachable under any condition? If yes, it represents an untested path.

Analyzing Untested Paths

Not all untested paths are equally important. Prioritize:

  • Error handling code (e.g., exception handlers, fallback routines) — often left untested but critical for safe operation.
  • Edge cases and boundary conditions — loops that never iterate, array indices at limits, default cases in switch statements.
  • Safety-related functions — code that monitors sensors, actuates outputs, or checks invariants.

Use coverage reports to identify specific sequences: a red branch inside a nested if indicates a path that never happens in any test. Write new test cases that force that condition to be true (or false) by providing appropriate input data.

Choosing the right tool depends on your language, platform, and coverage requirements.

gcov (C/C++)

gcov is the GNU coverage tool bundled with GCC. It provides line and branch coverage and is free and open source. It integrates well with build systems like CMake and can be used in cross-compilation for embedded targets. Official gcov documentation explains how to generate reports.

JaCoCo (Java)

JaCoCo is the industry-standard coverage library for Java. It offers line, branch, and method coverage. Its agent can attach to running JVMs without code changes. For engineering systems built on Java (e.g., SCADA or simulation frameworks), JaCoCo is highly effective. JaCoCo website has detailed configuration guides.

BullseyeCoverage (C/C++)

BullseyeCoverage is a commercial tool that provides function, branch, and condition coverage (including MC/DC). It is designed for safety-critical and embedded development. Its reports show exactly which conditions within expressions are untested. Many teams in aerospace and automotive rely on BullseyeCoverage for DO-178C and ISO 26262 compliance.

Other Tools

  • OpenCppCoverage (Windows, C/C++) — a free, open-source tool that integrates with Visual Studio and offers branch coverage.
  • Coverage.py (Python) — for data-driven engineering scripts written in Python, this tool provides line and branch coverage.
  • Go's built-in coverage — Go's go test -cover provides line and statement coverage, with experimental branch support.

Many teams also use cloud-based aggregation services like Codecov or SonarQube to visualize coverage trends and gate pull requests.

Integrating Coverage into Development Workflow

Identifying untested paths should be a continuous activity, not a one-time audit. Embed coverage analysis into your CI/CD pipeline:

  • Run coverage on every commit — even partial coverage gives rapid feedback.
  • Set minimum coverage thresholds — fail builds if coverage drops below a configurable level (e.g., 80% branch coverage for critical modules).
  • Generate coverage reports as artifacts — make them accessible to all developers.
  • Create coverage diffs — tools like Codecov show which lines a new pull request touches that are untested, forcing developers to add tests for uncovered changes.
  • Gate merges on untested paths — for high-integrity software, require 100% MC/DC coverage for safety-critical functions before merging.

Automation removes the burden of manual inspection. Developers can see untested paths highlighted in their editor or in the CI dashboard and write tests immediately.

Best Practices for Effective Use

To get the most out of code coverage tools for finding untested paths, follow these practices:

  • Combine multiple coverage types — line coverage alone can be deceptive. Use branch and condition coverage to uncover deeper untested paths.
  • Focus on high-risk code — target complex functions, error handlers, and security-sensitive routines. Not all code needs 100% coverage; focus on what matters.
  • Use static analysis alongside coverage — static analysis can find unreachable code that coverage tools may miss (e.g., dead code not executed because of logic errors). Together they provide a fuller picture.
  • Measure coverage under realistic conditions — use system-level and integration tests, not just unit tests. Untested paths often lie at the boundaries between modules.
  • Avoid coverage for coverage's sake — writing tests that artificially raise coverage without validating behavior (e.g., testing trivial getters/setters) wastes effort. Always tie untested paths to meaningful scenarios.
  • Review coverage trends over time — a decreasing coverage trend indicates that new code is being added without corresponding tests, creating new untested paths.
  • Educate the team — help developers understand that coverage reports are not a judgment but a tool for finding gaps. Foster a culture where addressing untested paths is valued.

Challenges and Limitations

While powerful, code coverage tools have limitations that teams must acknowledge:

  • Overhead — instrumentation can slow down test execution and increase binary size. For embedded systems with tight memory, this may be problematic. Compile-time instrumentation often has minimal runtime overhead but requires careful setup.
  • False confidence — high coverage does not mean perfect testing. Tests might exercise code but not check the results properly. Combine coverage with assertion density and mutation testing.
  • Path explosion — for highly complex code with many conditions, path coverage is computationally infeasible. Use MC/DC or branch coverage as a practical substitute.
  • Instrumentation in production — most coverage tools are designed for development testing. Deploying instrumented code to production is risky due to performance and security concerns.
  • Language and environment limitations — some embedded targets lack robust coverage tooling, especially for assembly or custom hardware.

Despite these challenges, code coverage remains one of the most effective ways to identify untested paths. The key is to use the tools intelligently and combine them with other verification methods.

Conclusion

Code coverage tools are indispensable for engineering software teams who need to ensure every critical path is tested. By revealing untested lines, branches, and conditions, they provide a data-driven way to focus testing efforts where they matter most. When integrated into CI/CD workflows and combined with static analysis and risk-based prioritization, coverage analysis helps prevent the hidden bugs that can lead to failures in the field. Whether you are developing avionics firmware, automotive control units, or industrial simulation software, systematic identification of untested paths using coverage tools should be a core part of your verification strategy. Start with the right tool for your language, set realistic coverage targets, and continuously monitor progress. Your software — and your users — will be safer for it.