본문으로 건너뛰기 C++ Code Coverage Complete Guide | gcov· lcov

C++ Code Coverage Complete Guide | gcov· lcov

C++ Code Coverage Complete Guide | gcov· lcov

이 글의 핵심

From C++ code coverage measurement to CI/CD integration. gcov, lcov, Codecov tool comparison, line/branch/function coverage analysis, test quality improvement strategy. Build practical workflow integrated with Google Test.

Introduction

“I wrote tests, but how do I know if they’re sufficient?” Code Coverage provides a quantitative answer to this question. It’s a powerful tool that measures the proportion of code actually executed by tests, finding untested areas. Unit testing is important in all languages. Python’s pytest·CI, Node.js Jest, C++ Google Test, Go’s go test, Rust’s cargo test are close to standards in their respective ecosystems. Putting coverage gates in CI is covered together in C++ GitHub Actions Multi-OS Build and Node.js GitHub Actions CI/CD.

Problem Scenario

int divide(int a, int b) {
    if (b == 0) {        // Line 1: Condition check
        return 0;        // Line 2: Error handling (not executed!)
    }
    return a / b;        // Line 3: Normal path
}
// Test code
TEST(DivideTest, NormalCase) {
    EXPECT_EQ(divide(10, 2), 5);  // Only test normal case
}
// Coverage measurement result:
// - Line 1: ✅ Executed (condition check)
// - Line 2: ❌ Not executed (error handling missing!)
// - Line 3: ✅ Executed (normal path)
// Line coverage: 66% (2/3)

Problem: Error case of dividing by zero is not tested! This guide covers everything from types of code coverage to measurement tools, CI/CD integration, and practical workflows.

Table of Contents

  1. Code Coverage Basic Concepts
  2. Coverage Types
  3. gcov Usage
  4. Visualization with lcov
  5. Google Test Integration
  6. Practical Workflow
  7. CI/CD Integration
  8. Tool Comparison
  9. Common Issues
  10. Best Practices
flowchart LR
    A[Source Code] --> B[Compile\n--coverage]
    B --> C[Run Tests]
    C --> D[".gcda files\ngenerated"]
    D --> E[gcov analysis]
    E --> F[Coverage\nReport]
    
    D --> G[lcov collect]
    G --> H[HTML\nReport]
    
    style F fill:#51cf66
    style H fill:#4dabf7

Code Coverage Basic Concepts

Code coverage measures the proportion of code executed by tests. It is reported as a percentage (executed lines / total executable lines), collected by instrumenting the binary at compile time and recording which lines, branches, and functions actually ran during a test suite. It answers “did this code run at all?” — not “is this code correct?” — which is why coverage is a floor for test quality, not a ceiling.

Coverage Types

Different coverage metrics catch different gaps — a high line-coverage number can still hide untested branches.

Line Coverage

The simplest and most common metric: what fraction of executable source lines ran at least once.

int classify(int score) {
    if (score >= 90) return 1;       // line covered if any test hits 90+
    return 0;                         // line covered if any test hits <90
}

Branch Coverage

Stricter than line coverage — a single if line can be “covered” by line coverage while only ever being tested in one direction (always true, or always false).

int classify(int score) {
    if (score >= 90) return 1;  // ← both true AND false paths must run
    return 0;                    // for 100% branch coverage on this line
}
// TEST(Classify, High) { EXPECT_EQ(classify(95), 1); }   // true branch
// TEST(Classify, Low)  { EXPECT_EQ(classify(50), 0); }   // false branch

Function Coverage

Tracks whether each function was called at all — the coarsest metric, useful for quickly spotting entirely untested functions (e.g. error-handling helpers, rarely used utility functions).

Condition Coverage

For compound conditions (a && b, a || b), condition coverage checks that each sub-expression was independently evaluated both true and false — branch coverage alone can be satisfied without ever proving b matters when a is true.

bool isValidUser(bool hasAccount, bool isActive) {
    return hasAccount && isActive;  // condition coverage needs 4 cases,
                                      // not just 2 (branch coverage)
}

Which to Target

MetricStrictnessTypical target
Function coverageLoosest95%+ (easy to hit, flags dead code)
Line coverageModerate70–90%
Branch coverageStrict60–80% (harder to reach 100% meaningfully)
Condition coverageStrictestReserved for safety-critical logic

gcov Usage (GCC Built-in Tool)

gcov is a code coverage analysis tool built into GCC.

Basic Workflow

# 1. Compile with coverage option
g++ --coverage program.cpp -o program
# Or
g++ -fprofile-arcs -ftest-coverage program.cpp -o program
# 2. Run program (run tests)
./program
# 3. Check coverage data files generated
ls *.gcda *.gcno
# program.gcno: Generated at compile time (graph info)
# program.gcda: Generated at runtime (execution count)
# 4. Analyze with gcov
gcov program.cpp
# 5. Check result file
cat program.cpp.gcov

Note: Using with optimization (-O2 or higher) can misalign line mapping, so often have a dedicated build type for coverage.

Practical Example: Calculator Program

// calculator.cpp
#include <iostream>
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}
int multiply(int a, int b) {
    return a * b;
}
int divide(int a, int b) {
    if (b == 0) {
        std::cerr << "Error: Division by zero\n";
        return 0;
    }
    return a / b;
}
int main() {
    std::cout << "10 + 5 = " << add(10, 5) << "\n";
    std::cout << "10 - 5 = " << subtract(10, 5) << "\n";
    std::cout << "10 * 5 = " << multiply(10, 5) << "\n";
    // divide function not called!
    return 0;
}
# Compile and run
$ g++ --coverage calculator.cpp -o calculator
$ ./calculator
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
# Coverage analysis
$ gcov calculator.cpp
File 'calculator.cpp'
Lines executed:73.33% of 15
Creating 'calculator.cpp.gcov'
# Check detailed report
$ cat calculator.cpp.gcov
        -:    0:Source:calculator.cpp
        -:    1:#include <iostream>
        -:    2:
        1:    3:int add(int a, int b) {
        1:    4:    return a + b;
        -:    5:}
        -:    6:
        1:    7:int subtract(int a, int b) {
        1:    8:    return a - b;
        -:    9:}
        -:   10:
        1:   11:int multiply(int a, int b) {
        1:   12:    return a * b;
        -:   13:}
        -:   14:
    #####:   15:int divide(int a, int b) {  // ##### = not executed!
    #####:   16:    if (b == 0) {
    #####:   17:        std::cerr << "Error: Division by zero\n";
    #####:   18:        return 0;
        -:   19:    }
    #####:   20:    return a / b;
        -:   21:}
        -:   22:
        1:   23:int main() {
        1:   24:    std::cout << "10 + 5 = " << add(10, 5) << "\n";
        1:   25:    std::cout << "10 - 5 = " << subtract(10, 5) << "\n";
        1:   26:    std::cout << "10 * 5 = " << multiply(10, 5) << "\n";
        1:   27:    return 0;
        -:   28:}
# Interpretation:
# - "1:": Executed once
# - "#####:": Not executed (uncovered)
# - "-:": Non-executable line (comments, declarations, etc.)

gcov Advanced Options

# Include branch coverage
gcov -b calculator.cpp
# Output:
# Function 'divide'
# Lines executed:0.00% of 5
# Branches executed:0.00% of 2
# Taken at least once:0.00% of 2
# Function-level coverage
gcov -f calculator.cpp
# Output:
# Function 'add'
# Lines executed:100.00% of 1
# 
# Function 'subtract'
# Lines executed:100.00% of 1
# 
# Function 'multiply'
# Lines executed:100.00% of 1
# 
# Function 'divide'
# Lines executed:0.00% of 5
# Show only unexecuted lines
gcov -u calculator.cpp
# All options combined
gcov -b -f -u calculator.cpp

CMake Integration

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Add coverage build type
if(CMAKE_BUILD_TYPE STREQUAL "Coverage")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
add_executable(myapp src/main.cpp src/math.cpp)
# Add coverage target
if(CMAKE_BUILD_TYPE STREQUAL "Coverage")
    add_custom_target(coverage
        COMMAND ${CMAKE_CURRENT_BINARY_DIR}/myapp
        COMMAND gcov ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Generating coverage report"
    )
endif()
# Build and generate coverage
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Coverage ..
make
make coverage

Visualization with lcov (HTML Report)

lcov is a tool that collects gcov data and generates beautiful HTML reports.

Installation

# Ubuntu/Debian
sudo apt install lcov
# macOS
brew install lcov
# Arch Linux
sudo pacman -S lcov

Basic Usage

# 1. Compile with coverage option
g++ --coverage src/*.cpp -o myapp -lgtest -lgtest_main
# 2. Run tests
./myapp
# 3. Collect coverage data
lcov --capture --directory . --output-file coverage.info
# 4. Generate HTML report
genhtml coverage.info --output-directory coverage_html
# 5. View in browser
open coverage_html/index.html  # macOS
xdg-open coverage_html/index.html  # Linux
start coverage_html/index.html  # Windows (Git Bash)

Google Test Integration

Complete example using Google Test with coverage.

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(StringUtilsProject)
set(CMAKE_CXX_STANDARD 17)
# Download Google Test
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
# Source library
add_library(string_utils src/string_utils.cpp)
target_include_directories(string_utils PUBLIC src)
# Test executable
add_executable(string_utils_test test/string_utils_test.cpp)
target_link_libraries(string_utils_test string_utils gtest gtest_main)
# Coverage settings
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
    target_compile_options(string_utils PRIVATE --coverage)
    target_link_options(string_utils PRIVATE --coverage)
    target_compile_options(string_utils_test PRIVATE --coverage)
    target_link_options(string_utils_test PRIVATE --coverage)
    
    # Coverage target
    find_program(LCOV lcov REQUIRED)
    find_program(GENHTML genhtml REQUIRED)
    
    add_custom_target(coverage
        COMMAND ${LCOV} --directory . --zerocounters
        COMMAND $<TARGET_FILE:string_utils_test>
        COMMAND ${LCOV} --capture --directory . --output-file coverage.info
        COMMAND ${LCOV} --remove coverage.info '/usr/*' '*/test/*' '*/googletest/*' 
                --output-file coverage_filtered.info
        COMMAND ${GENHTML} coverage_filtered.info --output-directory coverage_html
        COMMAND ${LCOV} --summary coverage_filtered.info
        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        COMMENT "Generating coverage report"
        DEPENDS string_utils_test
    )
endif()
# CTest integration
enable_testing()
add_test(NAME string_utils_test COMMAND string_utils_test)

Build and Run

# Build with coverage enabled
mkdir build && cd build
cmake -DENABLE_COVERAGE=ON ..
make
# Run tests
./string_utils_test
# Output:
# [==========] Running 4 tests from 1 test suite.
# [----------] Global test environment set-up.
# [----------] 4 tests from StringUtilsTest
# [ RUN      ] StringUtilsTest.TrimSpaces
# [       OK ] StringUtilsTest.TrimSpaces (0 ms)
# [ RUN      ] StringUtilsTest.SplitString
# [       OK ] StringUtilsTest.SplitString (0 ms)
# [ RUN      ] StringUtilsTest.StartsWith
# [       OK ] StringUtilsTest.StartsWith (0 ms)
# [ RUN      ] StringUtilsTest.EndsWith
# [       OK ] StringUtilsTest.EndsWith (0 ms)
# [----------] 4 tests from StringUtilsTest (0 ms total)
# [==========] 4 tests from 1 test suite ran. (0 ms total)
# [  PASSED  ] 4 tests.
# Generate coverage report
make coverage
# Output:
# Generating coverage report
# Overall coverage rate:
#   lines......: 100.0% (24 of 24 lines)
#   functions..: 100.0% (4 of 4 functions)
# View HTML report
open coverage_html/index.html

Practical Workflow

Daily Development Workflow

# 1. Develop new feature
vim src/new_feature.cpp
# 2. Write tests
vim test/new_feature_test.cpp
# 3. Build and test
mkdir -p build && cd build
cmake -DENABLE_COVERAGE=ON ..
make
./test_runner
# 4. Check coverage
make coverage
# 5. Check uncovered parts
open coverage_html/index.html
# 6. Write additional tests (uncovered parts)
vim test/new_feature_test.cpp
# 7. Test and check coverage again
make && make coverage
# 8. Commit when goal achieved
git add .
git commit -m "Add new feature with 90% coverage"

CI/CD Integration

GitHub Actions Workflow

Running coverage on every PR and uploading the report gives reviewers a visible signal without anyone needing to run lcov locally.

# .github/workflows/coverage.yml
name: Coverage
on: [pull_request]
jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: sudo apt-get install -y lcov
      - name: Build with coverage
        run: |
          mkdir build && cd build
          cmake -DENABLE_COVERAGE=ON ..
          make
      - name: Run tests
        run: cd build && ./string_utils_test
      - name: Generate coverage report
        run: |
          cd build
          lcov --capture --directory . --output-file coverage.info
          lcov --remove coverage.info '/usr/*' '*/test/*' '*/googletest/*' \
               --output-file coverage_filtered.info
      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: build/coverage_filtered.info

Failing the Build Below a Threshold

Gating merges on a coverage floor prevents a slow erosion of test quality — lcov --summary combined with a small parsing step is enough to enforce it without a paid service.

#!/bin/bash
# check_coverage.sh
THRESHOLD=70
COVERAGE=$(lcov --summary coverage_filtered.info 2>&1 | grep "lines" | grep -oP '\d+\.\d+(?=%)')
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
    echo "❌ Coverage $COVERAGE% is below threshold $THRESHOLD%"
    exit 1
fi
echo "✅ Coverage $COVERAGE% meets threshold $THRESHOLD%"

Coverage Diff on Pull Requests

Codecov and similar services can annotate a PR with only the coverage delta introduced by that PR’s changed lines — more actionable than a single project-wide percentage, since it flags exactly the new code that lacks tests.

      - uses: codecov/codecov-action@v4
        with:
          files: build/coverage_filtered.info
          fail_ci_if_error: true
          # codecov.yml can set: project target auto, patch target 80%

Tool Comparison

ToolTypeOutputStrengthsBest for
gcovGCC built-inText (.gcov files)No extra install, always available with GCCQuick local checks
lcovgcov frontendHTMLVisual, drill-down by file/functionLocal development, PR review
llvm-covClang built-inText/HTML/JSONWorks with Clang’s source-based coverage, faster than gcov on large projectsClang-based toolchains
CodecovSaaSWeb dashboard, PR commentsHistorical trends, PR diffs, badgesTeam/CI visibility
CoverallsSaaSWeb dashboard, PR commentsSimilar to Codecov, simpler free tierSmall/open-source projects
OpenCppCoverageStandaloneHTML/Cobertura XMLWorks with MSVC, no --coverage flag neededWindows/MSVC projects

gcov vs llvm-cov

If the project builds with Clang, llvm-cov (source-based coverage via -fprofile-instr-generate -fcoverage-mapping) is generally preferred over piping through gcov-compatible output — it is faster and gives more accurate branch information for Clang-compiled code.

clang++ -fprofile-instr-generate -fcoverage-mapping test.cpp -o test
LLVM_PROFILE_FILE="test.profraw" ./test
llvm-profdata merge -sparse test.profraw -o test.profdata
llvm-cov show ./test -instr-profile=test.profdata

Common Issues

Coverage Shows 0% Despite Running Tests

Usually means the .gcda files were generated in a different directory than where lcov/gcov is looking, or a stale build wasn’t recompiled with --coverage.

# Clean stale coverage data before re-running
find . -name "*.gcda" -delete
lcov --directory . --zerocounters
make && ./test_runner
lcov --capture --directory . --output-file coverage.info

Coverage Numbers Change Between Runs

Coverage should be deterministic for a fixed test suite — non-determinism usually means parallel test execution is racing on the same .gcda files, or leftover data from a previous run wasn’t cleared.

# Run tests serially when measuring coverage, or isolate gcda output per-process
GCOV_PREFIX=/tmp/cov-$$ GCOV_PREFIX_STRIP=99 ./test_runner

Template-Heavy Code Shows Misleading Coverage

Each template instantiation can report separately, so a templated function tested with only int may show as “covered” while the std::string instantiation used elsewhere in production is never exercised — coverage tools generally cannot distinguish between instantiations in the summary percentage.

--remove Filters Nothing / Third-Party Code Pollutes the Report

lcov --remove patterns must match the absolute paths stored in coverage.info, which can differ from what looks intuitive when the build uses out-of-tree or symlinked source directories.

# Inspect actual stored paths first
lcov --list coverage.info | head -5
# Then match the real prefix, not an assumed one
lcov --remove coverage.info '/usr/*' "$(pwd)/build/_deps/*" --output-file coverage_filtered.info

Coverage Build Is Much Slower or Larger

--coverage instrumentation adds counters to every basic block, which increases both binary size and runtime — expected, and not a sign of misconfiguration. Keep a separate Coverage CMake build type (as shown earlier) so release builds stay uninstrumented.

Summary

Key Points

  1. Code Coverage: Measures proportion of code executed by tests
  2. gcov: GCC’s built-in coverage tool
  3. lcov: Frontend tool generating HTML reports
  4. Google Test: C++ testing framework
  5. CI/CD: Automate coverage measurement and reporting

Best Practices

  • Set realistic coverage targets (70-90%)
  • Focus on critical business logic
  • Don’t chase 100% coverage blindly
  • Use coverage as a guide, not a goal
  • Integrate into CI/CD pipeline
  • Review coverage reports regularly

Next Steps



Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. From C++ code coverage measurement to CI/CD integration.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, code-coverage, testing, gcov, lcov, quality, CI/CD.