Coming Soon
This lesson is currently being developed
Introduction to testing your code
Learn systematic approaches to testing C++ programs.
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.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
- What is the main purpose of software testing?
- What's the difference between manual testing and automated testing?
- What are the three main types of test cases you should consider?
- In Test-Driven Development, when do you write the tests?
- Why is regression testing important when you fix bugs?
Practice exercises
Try creating tests for these functions:
- Write tests for a
getGrade(int score)
function that returns 'A', 'B', 'C', 'D', or 'F' - Create a test suite for a
isPrime(int num)
function that checks if a number is prime - Test a
calculateInterest(double principal, double rate, int years)
function with various inputs - Build a simple calculator function and write comprehensive tests for all operations (+, -, *, /)
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions