Coming Soon

This lesson is currently being developed

Introduction to testing your code

Learn systematic approaches to testing C++ programs.

Error Detection and Handling
Chapter
Beginner
Difficulty
40min
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.1 — Introduction to testing your code

In this lesson, you'll learn the fundamentals of testing C++ programs to ensure they work correctly and catch errors before they become problems.

What is software testing?

Software testing is the process of verifying that your program works as expected under various conditions. Think of it like a quality inspector in a factory - they test products to make sure they meet standards before they're shipped to customers.

Testing helps you:

  • Find bugs before users encounter them
  • Ensure your program handles different inputs correctly
  • Verify that changes don't break existing functionality
  • Build confidence in your code's reliability
  • Save time by catching problems early

Types of testing

Manual testing

Manual testing involves running your program yourself and checking the results:

#include <iostream>

int add(int a, int b)
{
    return a + b;
}

int main()
{
    // Manual test cases
    std::cout << "Testing add function:" << std::endl;
    std::cout << "add(2, 3) = " << add(2, 3) << " (expected: 5)" << std::endl;
    std::cout << "add(-1, 1) = " << add(-1, 1) << " (expected: 0)" << std::endl;
    std::cout << "add(0, 0) = " << add(0, 0) << " (expected: 0)" << std::endl;
    
    return 0;
}

Output:

Testing add function:
add(2, 3) = 5 (expected: 5)
add(-1, 1) = 0 (expected: 0)
add(0, 0) = 0 (expected: 0)

Automated testing

Automated testing uses code to test other code, making the process faster and more reliable:

#include <iostream>

int add(int a, int b)
{
    return a + b;
}

bool testAdd()
{
    // Test case 1: positive numbers
    if (add(2, 3) != 5)
    {
        std::cout << "FAIL: add(2, 3) returned " << add(2, 3) << ", expected 5" << std::endl;
        return false;
    }
    
    // Test case 2: negative and positive
    if (add(-1, 1) != 0)
    {
        std::cout << "FAIL: add(-1, 1) returned " << add(-1, 1) << ", expected 0" << std::endl;
        return false;
    }
    
    // Test case 3: zero values
    if (add(0, 0) != 0)
    {
        std::cout << "FAIL: add(0, 0) returned " << add(0, 0) << ", expected 0" << std::endl;
        return false;
    }
    
    std::cout << "All add() tests passed!" << std::endl;
    return true;
}

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

Output:

All add() tests passed!

Test cases and edge cases

A test case is a specific scenario you want to test. Good test cases include:

Normal cases

Test typical inputs your function will receive:

#include <iostream>

int divide(int a, int b)
{
    return a / b;
}

void testDivideNormal()
{
    std::cout << "Testing normal cases:" << std::endl;
    std::cout << "divide(10, 2) = " << divide(10, 2) << " (expected: 5)" << std::endl;
    std::cout << "divide(15, 3) = " << divide(15, 3) << " (expected: 5)" << std::endl;
    std::cout << "divide(7, 2) = " << divide(7, 2) << " (expected: 3)" << std::endl;
}

Boundary cases

Test values at the limits of acceptable input:

#include <iostream>
#include <climits>

bool isPositive(int num)
{
    return num > 0;
}

void testBoundaryCases()
{
    std::cout << "Testing boundary cases:" << std::endl;
    std::cout << "isPositive(0) = " << isPositive(0) << " (expected: 0)" << std::endl;
    std::cout << "isPositive(1) = " << isPositive(1) << " (expected: 1)" << std::endl;
    std::cout << "isPositive(-1) = " << isPositive(-1) << " (expected: 0)" << std::endl;
    std::cout << "isPositive(INT_MAX) = " << isPositive(INT_MAX) << " (expected: 1)" << std::endl;
    std::cout << "isPositive(INT_MIN) = " << isPositive(INT_MIN) << " (expected: 0)" << std::endl;
}

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

Output:

Testing boundary cases:
isPositive(0) = 0 (expected: 0)
isPositive(1) = 1 (expected: 1)
isPositive(-1) = 0 (expected: 0)
isPositive(INT_MAX) = 1 (expected: 1)
isPositive(INT_MIN) = 0 (expected: 0)

Error cases

Test invalid or problematic inputs:

#include <iostream>

double safeDivide(double a, double b, bool& success)
{
    if (b == 0.0)
    {
        success = false;
        return 0.0;
    }
    
    success = true;
    return a / b;
}

void testErrorCases()
{
    std::cout << "Testing error cases:" << std::endl;
    
    bool success;
    double result = safeDivide(10.0, 0.0, success);
    
    if (success)
        std::cout << "FAIL: Division by zero should fail!" << std::endl;
    else
        std::cout << "PASS: Division by zero correctly handled" << std::endl;
    
    result = safeDivide(10.0, 2.0, success);
    if (success && result == 5.0)
        std::cout << "PASS: Normal division works correctly" << std::endl;
    else
        std::cout << "FAIL: Normal division failed" << std::endl;
}

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

Output:

Testing error cases:
PASS: Division by zero correctly handled
PASS: Normal division works correctly

Creating a simple test framework

You can build a simple testing framework to organize your tests:

#include <iostream>
#include <string>

// Simple test framework
class TestFramework
{
private:
    int totalTests = 0;
    int passedTests = 0;
    
public:
    void expect(bool condition, const std::string& testName)
    {
        totalTests++;
        
        if (condition)
        {
            std::cout << "PASS: " << testName << std::endl;
            passedTests++;
        }
        else
        {
            std::cout << "FAIL: " << testName << std::endl;
        }
    }
    
    void printSummary()
    {
        std::cout << "\nTest Results: " << passedTests << "/" << totalTests << " passed" << std::endl;
        
        if (passedTests == totalTests)
            std::cout << "All tests passed! ✓" << std::endl;
        else
            std::cout << (totalTests - passedTests) << " tests failed! ✗" << std::endl;
    }
};

// Functions to test
int multiply(int a, int b)
{
    return a * b;
}

bool isEven(int num)
{
    return num % 2 == 0;
}

int main()
{
    TestFramework test;
    
    // Test multiply function
    test.expect(multiply(2, 3) == 6, "multiply(2, 3) should equal 6");
    test.expect(multiply(-2, 3) == -6, "multiply(-2, 3) should equal -6");
    test.expect(multiply(0, 5) == 0, "multiply(0, 5) should equal 0");
    
    // Test isEven function
    test.expect(isEven(2) == true, "isEven(2) should return true");
    test.expect(isEven(3) == false, "isEven(3) should return false");
    test.expect(isEven(0) == true, "isEven(0) should return true");
    test.expect(isEven(-2) == true, "isEven(-2) should return true");
    
    test.printSummary();
    
    return 0;
}

Output:

PASS: multiply(2, 3) should equal 6
PASS: multiply(-2, 3) should equal -6
PASS: multiply(0, 5) should equal 0
PASS: isEven(2) should return true
PASS: isEven(3) should return false
PASS: isEven(0) should return true
PASS: isEven(-2) should return true

Test Results: 7/7 passed
All tests passed! ✓

Testing strategies

Test-Driven Development (TDD)

Write tests before writing the actual function:

#include <iostream>

// First, write the test (this will fail initially)
bool testAbsoluteValue()
{
    // We haven't implemented absolute() yet!
    // return (absolute(-5) == 5) && (absolute(5) == 5) && (absolute(0) == 0);
    return false; // Placeholder
}

// Then implement the function to pass the test
int absolute(int num)
{
    if (num < 0)
        return -num;
    return num;
}

// Update the test to actually test the function
bool testAbsoluteValueReal()
{
    bool test1 = (absolute(-5) == 5);
    bool test2 = (absolute(5) == 5);
    bool test3 = (absolute(0) == 0);
    
    if (test1 && test2 && test3)
    {
        std::cout << "All absolute() tests passed!" << std::endl;
        return true;
    }
    
    std::cout << "Some absolute() tests failed!" << std::endl;
    return false;
}

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

Output:

All absolute() tests passed!

Regression testing

Test that fixes don't break existing functionality:

#include <iostream>

// Version 1: Original function
int calculateAreaV1(int length, int width)
{
    return length * width;  // Bug: doesn't handle negative values
}

// Version 2: Fixed function
int calculateAreaV2(int length, int width)
{
    // Fix: Handle negative values
    if (length < 0 || width < 0)
        return -1; // Error code
    
    return length * width;
}

void regressionTest()
{
    std::cout << "Regression testing calculateArea:" << std::endl;
    
    // Test that worked in V1 should still work in V2
    std::cout << "calculateAreaV2(3, 4) = " << calculateAreaV2(3, 4) << " (expected: 12)" << std::endl;
    std::cout << "calculateAreaV2(5, 6) = " << calculateAreaV2(5, 6) << " (expected: 30)" << std::endl;
    
    // Test the new fix
    std::cout << "calculateAreaV2(-1, 4) = " << calculateAreaV2(-1, 4) << " (expected: -1)" << std::endl;
    std::cout << "calculateAreaV2(3, -4) = " << calculateAreaV2(3, -4) << " (expected: -1)" << std::endl;
}

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

Output:

Regression testing calculateArea:
calculateAreaV2(3, 4) = 12 (expected: 12)
calculateAreaV2(5, 6) = 30 (expected: 30)
calculateAreaV2(-1, 4) = -1 (expected: -1)
calculateAreaV2(3, -4) = -1 (expected: -1)

Best practices for testing

1. Test early and often

#include <iostream>

// Write tests as you develop
int factorial(int n)
{
    if (n < 0)
        return -1; // Error for negative numbers
    if (n == 0 || n == 1)
        return 1;
    
    int result = 1;
    for (int i = 2; i <= n; ++i)
        result *= i;
    
    return result;
}

void testFactorial()
{
    std::cout << "Testing factorial function:" << std::endl;
    
    // Test base cases
    std::cout << "factorial(0) = " << factorial(0) << " (expected: 1)" << std::endl;
    std::cout << "factorial(1) = " << factorial(1) << " (expected: 1)" << std::endl;
    
    // Test normal cases
    std::cout << "factorial(3) = " << factorial(3) << " (expected: 6)" << std::endl;
    std::cout << "factorial(5) = " << factorial(5) << " (expected: 120)" << std::endl;
    
    // Test error case
    std::cout << "factorial(-1) = " << factorial(-1) << " (expected: -1)" << std::endl;
}

2. Use descriptive test names

void testDivisionByZeroReturnsError()
{
    // Clear what this test does from the name
}

void testPositiveNumbersReturnCorrectResult()
{
    // Self-documenting test name
}

3. Test one thing at a time

// Good: Each test focuses on one specific behavior
bool testAdditionWithPositiveNumbers()
{
    return add(2, 3) == 5;
}

bool testAdditionWithNegativeNumbers()
{
    return add(-2, -3) == -5;
}

// Poor: Testing too many things at once
bool testAdditionEverything()
{
    return add(2, 3) == 5 && add(-2, -3) == -5 && add(0, 0) == 0;
}

When testing finds bugs

When tests fail, they help you locate and fix problems:

#include <iostream>

// Function with a bug
int findMax(int a, int b, int c)
{
    if (a > b && a > c)
        return a;
    else if (b > c)  // Bug: should be (b > a && b > c)
        return b;
    else
        return c;
}

void testFindMax()
{
    std::cout << "Testing findMax function:" << std::endl;
    
    // This test will pass
    int result1 = findMax(1, 2, 3);
    std::cout << "findMax(1, 2, 3) = " << result1 << " (expected: 3) ";
    std::cout << (result1 == 3 ? "PASS" : "FAIL") << std::endl;
    
    // This test will fail and reveal the bug
    int result2 = findMax(3, 1, 2);
    std::cout << "findMax(3, 1, 2) = " << result2 << " (expected: 3) ";
    std::cout << (result2 == 3 ? "PASS" : "FAIL") << std::endl;
}

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

Output:

Testing findMax function:
findMax(1, 2, 3) = 3 (expected: 3) PASS
findMax(3, 1, 2) = 2 (expected: 3) FAIL

The failing test shows us there's a logic error in our findMax function!

Testing different data types

Test functions that work with various data types:

#include <iostream>
#include <string>

// Test string functions
bool startsWith(const std::string& str, const std::string& prefix)
{
    if (prefix.length() > str.length())
        return false;
    
    return str.substr(0, prefix.length()) == prefix;
}

void testStringFunctions()
{
    std::cout << "Testing string functions:" << std::endl;
    
    // Test normal cases
    std::cout << "startsWith(\"hello\", \"he\") = " << startsWith("hello", "he") << " (expected: 1)" << std::endl;
    std::cout << "startsWith(\"world\", \"wo\") = " << startsWith("world", "wo") << " (expected: 1)" << std::endl;
    
    // Test edge cases
    std::cout << "startsWith(\"\", \"\") = " << startsWith("", "") << " (expected: 1)" << std::endl;
    std::cout << "startsWith(\"hi\", \"hello\") = " << startsWith("hi", "hello") << " (expected: 0)" << std::endl;
    
    // Test failure cases
    std::cout << "startsWith(\"hello\", \"hi\") = " << startsWith("hello", "hi") << " (expected: 0)" << std::endl;
}

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

Output:

Testing string functions:
startsWith("hello", "he") = 1 (expected: 1)
startsWith("world", "wo") = 1 (expected: 1)
startsWith("", "") = 1 (expected: 1)
startsWith("hi", "hello") = 0 (expected: 0)
startsWith("hello", "hi") = 0 (expected: 0)

Summary

Testing is a crucial skill that helps you write reliable code:

  • Manual testing: Running programs yourself and checking results
  • Automated testing: Using code to test code systematically
  • Test cases: Include normal cases, boundary cases, and error cases
  • Test early: Write tests as you develop, don't wait until the end
  • Test often: Run tests frequently to catch problems quickly
  • Regression testing: Ensure fixes don't break existing functionality

Good testing practices save time by catching bugs early and give you confidence that your code works correctly. Start with simple manual tests, then build up to automated testing as your programs become more complex.

Quiz

  1. What is the main purpose of software testing?
  2. What's the difference between manual testing and automated testing?
  3. What are the three main types of test cases you should consider?
  4. In Test-Driven Development, when do you write the tests?
  5. Why is regression testing important when you fix bugs?

Practice exercises

Try creating tests for these functions:

  1. Write tests for a getGrade(int score) function that returns 'A', 'B', 'C', 'D', or 'F'
  2. Create a test suite for a isPrime(int num) function that checks if a number is prime
  3. Test a calculateInterest(double principal, double rate, int years) function with various inputs
  4. Build a simple calculator function and write comprehensive tests for all operations (+, -, *, /)

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