Coming Soon

This lesson is currently being developed

Code coverage

Understand how much of your code is being tested.

Error Detection and Handling
Chapter
Beginner
Difficulty
30min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

9.2 — Code coverage

In this lesson, you'll learn about code coverage - a metric that measures how much of your code is being tested and how to use it to improve your testing strategy.

What is code coverage?

Code coverage is a measurement that describes how much of your source code has been executed during testing. Think of it like a checklist - it tells you which parts of your program have been "visited" by your tests and which parts haven't.

Code coverage helps you:

  • Identify untested parts of your code
  • Find dead code (code that's never executed)
  • Ensure your tests are comprehensive
  • Build confidence in your test suite
  • Meet quality standards for software projects

Types of code coverage

Line coverage (Statement coverage)

Measures which lines of code have been executed:

#include <iostream>

int absoluteValue(int num)
{
    if (num < 0)           // Line 1
    {
        return -num;       // Line 2
    }
    return num;            // Line 3
}

void testAbsoluteValue()
{
    std::cout << "Testing absoluteValue:" << std::endl;
    
    // Test only positive numbers
    std::cout << "absoluteValue(5) = " << absoluteValue(5) << std::endl;
    std::cout << "absoluteValue(10) = " << absoluteValue(10) << std::endl;
}

int main()
{
    testAbsoluteValue();
    return 0;
}

Output:

Testing absoluteValue:
absoluteValue(5) = 5
absoluteValue(10) = 10

Coverage analysis:

  • Line 1 (if condition): ✓ Executed
  • Line 2 (return -num): ✗ Never executed
  • Line 3 (return num): ✓ Executed

Line coverage: 66% (2 out of 3 lines)

To achieve 100% line coverage, we need to test negative numbers too:

void betterTest()
{
    std::cout << "Better test coverage:" << std::endl;
    
    // Test positive numbers (executes line 1 and 3)
    std::cout << "absoluteValue(5) = " << absoluteValue(5) << std::endl;
    
    // Test negative numbers (executes line 1 and 2)
    std::cout << "absoluteValue(-5) = " << absoluteValue(-5) << std::endl;
    
    // Test zero (executes line 1 and 3)
    std::cout << "absoluteValue(0) = " << absoluteValue(0) << std::endl;
}

Output:

Better test coverage:
absoluteValue(5) = 5
absoluteValue(-5) = 5
absoluteValue(0) = 0

Now all lines are covered: 100% line coverage

Branch coverage (Decision coverage)

Measures whether both true and false branches of conditions have been tested:

#include <iostream>

std::string classifyNumber(int num)
{
    if (num > 0)
    {
        return "positive";
    }
    else if (num < 0)
    {
        return "negative";
    }
    else
    {
        return "zero";
    }
}

void testClassifyNumber()
{
    std::cout << "Testing classifyNumber:" << std::endl;
    
    // Only test positive numbers
    std::cout << "classifyNumber(5) = " << classifyNumber(5) << std::endl;
    std::cout << "classifyNumber(10) = " << classifyNumber(10) << std::endl;
}

int main()
{
    testClassifyNumber();
    return 0;
}

Branch coverage analysis:

  • if (num > 0) true branch: ✓ Tested
  • if (num > 0) false branch: ✗ Not tested
  • else if (num < 0) true branch: ✗ Not tested
  • else if (num < 0) false branch: ✗ Not tested
  • else branch: ✗ Not tested

Branch coverage: 20% (1 out of 5 branches)

Function coverage

Measures which functions have been called:

#include <iostream>

int add(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)
        return 0;
    return a / b;
}

int main()
{
    // Only test add function
    std::cout << "add(2, 3) = " << add(2, 3) << std::endl;
    
    // multiply and divide functions are never called
    return 0;
}

Function coverage: 33% (1 out of 3 functions)

Manual code coverage analysis

You can manually track coverage by marking which code paths your tests follow:

#include <iostream>

int calculateDiscount(double price, int customerType, bool hasCoupon)
{
    double discount = 0.0;
    
    // Path A: Regular customer
    if (customerType == 0)
    {
        discount = 0.05; // 5% discount
    }
    // Path B: Premium customer
    else if (customerType == 1)
    {
        discount = 0.10; // 10% discount
    }
    // Path C: VIP customer
    else
    {
        discount = 0.15; // 15% discount
    }
    
    // Path D: Additional coupon discount
    if (hasCoupon)
    {
        discount += 0.05; // Additional 5%
    }
    
    return static_cast<int>(price * discount * 100); // Return cents
}

void testCalculateDiscount()
{
    std::cout << "Testing calculateDiscount:" << std::endl;
    
    // Test case 1: Regular customer without coupon
    // Covers: Path A, !Path D
    int result1 = calculateDiscount(100.0, 0, false);
    std::cout << "Regular customer, no coupon: " << result1 << " cents" << std::endl;
    
    // Test case 2: Premium customer with coupon
    // Covers: Path B, Path D
    int result2 = calculateDiscount(100.0, 1, true);
    std::cout << "Premium customer, with coupon: " << result2 << " cents" << std::endl;
    
    // Missing test cases:
    // - VIP customer (Path C not tested)
    // - Regular customer with coupon
    // - Premium customer without coupon
}

int main()
{
    testCalculateDiscount();
    return 0;
}

Output:

Testing calculateDiscount:
Regular customer, no coupon: 5 cents
Premium customer, with coupon: 15 cents

Coverage analysis:

  • Path A (regular customer): ✓ Tested
  • Path B (premium customer): ✓ Tested
  • Path C (VIP customer): ✗ Not tested
  • Path D (with coupon): ✓ Tested
  • Path !D (without coupon): ✓ Tested

Creating a coverage tracking system

You can build a simple system to track which parts of your code are being tested:

#include <iostream>
#include <map>
#include <string>

class CoverageTracker
{
private:
    std::map<std::string, int> lineCounts;
    
public:
    void recordLine(const std::string& location)
    {
        lineCounts[location]++;
    }
    
    void printReport()
    {
        std::cout << "\n=== Coverage Report ===" << std::endl;
        for (const auto& pair : lineCounts)
        {
            std::cout << pair.first << ": executed " << pair.second << " times" << std::endl;
        }
    }
};

// Global coverage tracker
CoverageTracker coverage;

int gradeCalculator(int score)
{
    coverage.recordLine("gradeCalculator:start");
    
    if (score >= 90)
    {
        coverage.recordLine("gradeCalculator:A_grade");
        return 'A';
    }
    else if (score >= 80)
    {
        coverage.recordLine("gradeCalculator:B_grade");
        return 'B';
    }
    else if (score >= 70)
    {
        coverage.recordLine("gradeCalculator:C_grade");
        return 'C';
    }
    else if (score >= 60)
    {
        coverage.recordLine("gradeCalculator:D_grade");
        return 'D';
    }
    else
    {
        coverage.recordLine("gradeCalculator:F_grade");
        return 'F';
    }
}

void testGradeCalculator()
{
    std::cout << "Testing grade calculator:" << std::endl;
    
    // Test A grade
    std::cout << "Score 95: " << static_cast<char>(gradeCalculator(95)) << std::endl;
    
    // Test B grade
    std::cout << "Score 85: " << static_cast<char>(gradeCalculator(85)) << std::endl;
    
    // Test F grade
    std::cout << "Score 45: " << static_cast<char>(gradeCalculator(45)) << std::endl;
    
    // Note: C and D grades not tested!
}

int main()
{
    testGradeCalculator();
    coverage.printReport();
    
    return 0;
}

Output:

Testing grade calculator:
Score 95: A
Score 85: B
Score 45: F

=== Coverage Report ===
gradeCalculator:A_grade: executed 1 times
gradeCalculator:B_grade: executed 1 times
gradeCalculator:F_grade: executed 1 times
gradeCalculator:start: executed 3 times

Coverage analysis:

  • Lines covered: 4 out of 6 (66%)
  • Missing coverage: C_grade and D_grade paths

Loop coverage

Testing loops requires covering different scenarios:

#include <iostream>
#include <vector>

int sumPositiveNumbers(const std::vector<int>& numbers)
{
    int sum = 0;
    
    for (int num : numbers)  // Loop body
    {
        if (num > 0)         // Condition inside loop
        {
            sum += num;      // Addition path
        }
        // Negative numbers ignored (else path not explicit)
    }
    
    return sum;
}

void testSumPositiveNumbers()
{
    std::cout << "Testing sumPositiveNumbers:" << std::endl;
    
    // Test case 1: Empty vector (loop never executes)
    std::vector<int> empty = {};
    std::cout << "Empty vector: " << sumPositiveNumbers(empty) << std::endl;
    
    // Test case 2: All positive numbers (loop executes, if always true)
    std::vector<int> allPositive = {1, 2, 3, 4, 5};
    std::cout << "All positive: " << sumPositiveNumbers(allPositive) << std::endl;
    
    // Test case 3: All negative numbers (loop executes, if always false)
    std::vector<int> allNegative = {-1, -2, -3};
    std::cout << "All negative: " << sumPositiveNumbers(allNegative) << std::endl;
    
    // Test case 4: Mixed numbers (loop executes, if sometimes true/false)
    std::vector<int> mixed = {-2, 3, -1, 4, 0, 5};
    std::cout << "Mixed numbers: " << sumPositiveNumbers(mixed) << std::endl;
}

int main()
{
    testSumPositiveNumbers();
    return 0;
}

Output:

Testing sumPositiveNumbers:
Empty vector: 0
All positive: 15
All negative: 0
Mixed numbers: 12

Loop coverage achieved:

  • Loop never executes: ✓ (empty vector)
  • Loop executes once: ✓ (single element vectors would test this)
  • Loop executes multiple times: ✓ (all other test cases)
  • Loop condition true: ✓ (positive numbers)
  • Loop condition false: ✓ (negative numbers)

Coverage goals and limitations

What's a good coverage percentage?

Different coverage types have different targets:

#include <iostream>

// Simple function - should aim for 100% coverage
int max(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

// Complex function - 100% might be harder to achieve
double complexCalculation(double x, double y, int mode)
{
    if (mode == 0)
    {
        if (x > 0)
        {
            if (y > 0)
            {
                return x * y;
            }
            else
            {
                return x / 2.0;
            }
        }
        else
        {
            return 0.0;
        }
    }
    else if (mode == 1)
    {
        return x + y;
    }
    else
    {
        return x - y;
    }
}

Coverage targets:

  • Line coverage: Aim for 80-100%
  • Branch coverage: Aim for 70-90%
  • Function coverage: Aim for 90-100%

Coverage doesn't guarantee correctness

High coverage doesn't mean your tests are good:

#include <iostream>

int divide(int a, int b)
{
    return a / b;  // Bug: no check for division by zero
}

void badTest()
{
    // This achieves 100% line coverage but doesn't test the bug!
    std::cout << "divide(10, 2) = " << divide(10, 2) << std::endl;
}

void goodTest()
{
    // This also has 100% coverage AND tests the edge case
    std::cout << "divide(10, 2) = " << divide(10, 2) << std::endl;
    
    // This would crash the program, revealing the bug
    // std::cout << "divide(10, 0) = " << divide(10, 0) << std::endl;
    
    std::cout << "Note: Division by zero not safely handled!" << std::endl;
}

int main()
{
    std::cout << "Bad test (high coverage, poor quality):" << std::endl;
    badTest();
    
    std::cout << "\nGood test (high coverage, good quality):" << std::endl;
    goodTest();
    
    return 0;
}

Output:

Bad test (high coverage, poor quality):
divide(10, 2) = 5

Good test (high coverage, good quality):
divide(10, 2) = 5
Note: Division by zero not safely handled!

Using coverage to improve tests

Coverage reports help you identify what to test next:

#include <iostream>
#include <string>

std::string formatName(const std::string& first, const std::string& last, bool formal)
{
    if (formal)
    {
        if (last.empty())                    // Branch A1
        {
            return "Dear " + first;          // Line A1
        }
        else
        {
            return "Dear " + first + " " + last;  // Line A2
        }
    }
    else
    {
        if (first.empty() && last.empty())   // Branch B1
        {
            return "Anonymous";              // Line B1 (uncovered!)
        }
        else if (last.empty())              // Branch B2
        {
            return first;                    // Line B2
        }
        else
        {
            return first + " " + last;       // Line B3
        }
    }
}

void initialTests()
{
    std::cout << "Initial tests:" << std::endl;
    
    // Test formal with both names
    std::cout << formatName("John", "Doe", true) << std::endl;
    
    // Test informal with both names
    std::cout << formatName("Jane", "Smith", false) << std::endl;
    
    // Coverage gaps identified:
    // - Formal with empty last name (Branch A1, Line A1)
    // - Informal with empty first and last (Branch B1, Line B1)
    // - Informal with empty last name (Branch B2, Line B2)
}

void improvedTests()
{
    std::cout << "\nImproved tests (better coverage):" << std::endl;
    
    // Original tests
    std::cout << formatName("John", "Doe", true) << std::endl;
    std::cout << formatName("Jane", "Smith", false) << std::endl;
    
    // Fill coverage gaps
    std::cout << formatName("John", "", true) << std::endl;      // Cover A1
    std::cout << formatName("", "", false) << std::endl;        // Cover B1
    std::cout << formatName("Jane", "", false) << std::endl;    // Cover B2
}

int main()
{
    initialTests();
    improvedTests();
    
    return 0;
}

Output:

Initial tests:
Dear John Doe
Jane Smith

Improved tests (better coverage):
Dear John Doe
Jane Smith
Dear John
Anonymous
Jane

Coverage best practices

1. Use coverage to guide testing, not replace thinking

#include <iostream>

// Coverage can't tell you about missing requirements
int calculateTax(double income, int dependents)
{
    double tax = income * 0.20;  // 20% tax rate
    
    if (dependents > 0)
    {
        tax -= dependents * 1000;  // $1000 deduction per dependent
    }
    
    if (tax < 0)
        return 0;
    
    return static_cast<int>(tax);
}

void coverageBasedTests()
{
    std::cout << "Coverage-based tests achieve 100% line coverage:" << std::endl;
    
    std::cout << "Tax for $50000, 0 dependents: $" << calculateTax(50000, 0) << std::endl;
    std::cout << "Tax for $30000, 5 dependents: $" << calculateTax(30000, 5) << std::endl;
    
    // But these tests miss important scenarios:
    // - What about negative income?
    // - What about negative dependents?
    // - What about very high income?
    // - Are the tax rates correct?
}

int main()
{
    coverageBasedTests();
    return 0;
}

2. Combine different coverage types

#include <iostream>

bool isValidTriangle(int a, int b, int c)
{
    // Need positive values (data validation)
    if (a <= 0 || b <= 0 || c <= 0)
        return false;
    
    // Triangle inequality theorem (business logic)
    if (a + b <= c || a + c <= b || b + c <= a)
        return false;
    
    return true;
}

void comprehensiveTests()
{
    std::cout << "Comprehensive triangle tests:" << std::endl;
    
    // Line coverage: Hit all return statements
    std::cout << "Valid triangle (3,4,5): " << isValidTriangle(3, 4, 5) << std::endl;
    std::cout << "Invalid (negative): " << isValidTriangle(-1, 2, 3) << std::endl;
    std::cout << "Invalid (triangle inequality): " << isValidTriangle(1, 1, 5) << std::endl;
    
    // Branch coverage: Test all conditions true and false
    std::cout << "Zero side: " << isValidTriangle(0, 1, 1) << std::endl;
    std::cout << "Valid triangle (5,12,13): " << isValidTriangle(5, 12, 13) << std::endl;
    
    // Edge cases based on understanding the problem
    std::cout << "Equilateral triangle: " << isValidTriangle(5, 5, 5) << std::endl;
    std::cout << "Just invalid: " << isValidTriangle(1, 2, 3) << std::endl;
}

int main()
{
    comprehensiveTests();
    return 0;
}

Output:

Comprehensive triangle tests:
Valid triangle (3,4,5): 1
Invalid (negative): 0
Invalid (triangle inequality): 0
Zero side: 0
Valid triangle (5,12,13): 1
Equilateral triangle: 1
Just invalid: 0

Summary

Code coverage is a valuable tool for improving your testing:

  • Line coverage: Measures which lines of code are executed
  • Branch coverage: Measures which decision paths are taken
  • Function coverage: Measures which functions are called
  • Loop coverage: Ensures loops are tested in various scenarios

Key principles:

  • Use coverage to identify gaps in your tests
  • High coverage doesn't guarantee good tests
  • Combine coverage metrics with thoughtful test design
  • Aim for 80-100% line coverage, 70-90% branch coverage
  • Focus on testing important business logic and edge cases

Coverage is a guide, not a goal. The real objective is building confidence that your code works correctly in all important scenarios.

Quiz

  1. What does code coverage measure?
  2. What's the difference between line coverage and branch coverage?
  3. If a function has 100% line coverage but only 50% branch coverage, what does that tell you?
  4. Why might 100% code coverage not guarantee your code is bug-free?
  5. What are some techniques for improving code coverage?

Practice exercises

Try analyzing and improving coverage for these functions:

  1. Create coverage tests for a getMonthName(int month) function that returns month names
  2. Analyze branch coverage for a calculateShipping(double weight, bool express, bool international) function
  3. Test loop coverage for a findLargest(std::vector<int> numbers) function
  4. Build a simple coverage tracker for a passwordStrength(std::string password) function

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion