Coming Soon
This lesson is currently being developed
Code coverage
Understand how much of your code is being tested.
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
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: ✓ Testedif (num > 0)
false branch: ✗ Not testedelse if (num < 0)
true branch: ✗ Not testedelse if (num < 0)
false branch: ✗ Not testedelse
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
- What does code coverage measure?
- What's the difference between line coverage and branch coverage?
- If a function has 100% line coverage but only 50% branch coverage, what does that tell you?
- Why might 100% code coverage not guarantee your code is bug-free?
- What are some techniques for improving code coverage?
Practice exercises
Try analyzing and improving coverage for these functions:
- Create coverage tests for a
getMonthName(int month)
function that returns month names - Analyze branch coverage for a
calculateShipping(double weight, bool express, bool international)
function - Test loop coverage for a
findLargest(std::vector<int> numbers)
function - Build a simple coverage tracker for a
passwordStrength(std::string password)
function
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions