Unit testing is a foundational practice in software development that involves testing individual components or units of code to ensure they function correctly in isolation. This approach provides several key advantages, especially in projects aiming for high-quality, maintainable code. By validating each unit of code independently, developers can catch errors early in the development process, which minimizes the risk of bugs escalating to later stages. Detecting issues at an early stage helps reduce the time and costs associated with debugging complex codebases, as developers don’t have to trace problems across interconnected components.
Another significant advantage of unit testing is that it enhances code reliability and stability. Since each unit test checks the behavior of a small, specific part of the application, developers gain confidence that individual functions and methods are working as intended. Over time, as more tests are added, this testing framework becomes an automatic verification tool. When code changes are made, unit tests serve as a safeguard by identifying unintended side effects and regressions, allowing developers to refactor or extend the codebase without fear of breaking existing functionality. This makes unit testing especially useful in agile development environments, where frequent updates and modifications are part of the development process.
Unit tests also play a crucial role in documentation and collaboration. Well-written unit tests serve as a form of executable documentation that shows how different parts of the code are expected to behave. This is particularly useful for new team members who need to understand how specific functions work without reading through all the implementation details. Furthermore, with a solid suite of unit tests, teams can adopt practices like test-driven development (TDD), where tests are written before the actual code. TDD not only drives cleaner, more modular design but also ensures that all features are thoroughly tested from the outset, fostering a culture of quality and accountability in the development process.
Frameworks exist that can help us to easily write unit tests. Here we will explore one called CPPUnit for use with C++ code. You can find CppUnit at the website https://freedesktop.org/wiki/Software/cppunit/.
After you install CppUnit, add the following lines to your project's cmake CMakeLists.txt
file:
include_directories(/opt/local/include)
link_directories(/opt/local/lib)
target_link_libraries(CppUnitExamples cppunit)
or on MacOS you my need to specify the full path to the dynamic library file:
target_link_libraries(CppUnitExamples /opt/local/lib/libcppunit.dylib)
Be sure to replace <your_project_name>
with the actual name of your project.
Library to Test
In this post we will create a library with a number of global functions that we will test. Each function will pose different testing challenges that we will address. The first function that we test is a function that determines if an integral number is prime or not.
GlobalFunctions.cpp
/** * @brief Determines if a number is prime. * * This function checks if the given integer `n` is a prime number. A prime * number is a natural number greater than 1 that has no positive divisors * other than 1 and itself. * * @param n The integer to check for primality. * @return `true` if the number is prime, `false` otherwise. */ bool isPrime(long n) { if (n <= 1) { return false; // Numbers less than or equal to 1 are not prime } if (n <= 3) { return true; // 2 and 3 are prime numbers } if (n % 2 == 0 || n % 3 == 0) { return false; // Exclude multiples of 2 and 3 } // Check for factors up to the square root of `n` for (int i = 5; i <= std::sqrt(n); i += 6) { if (n % i == 0 || n % (i + 2) == 0) { return false; } } return true; }
This function has a number of if
statements and a for
loop that create a number of different code paths to test. We will need to test all the code paths if we are to have confidence that the function is working properly in all cases. To do this we partition the input space of the single long
argument and write tests for each partition. The partitions are as follows:
- Numbers <=1 that should return false
- 2 and 3 which should return true
- Multiples of 2 or 3 which should return false
- Other larger values which are prime that should return true
- Other larger values which are not prime that should return false
Test Runner
To start we need to create a test runner main()
method to act as the initiating method for running the tests.
testrunner.cpp
#include <cppunit/ui/text/TestRunner.h> #include "IsPrimeTests.h" int main() { CppUnit::TextUi::TestRunner runner; // Register the test suite runner.addTest(IsPrimeTests::suite()); // Run the tests bool wasSuccessful = runner.run(); // Return whether all tests passed or failed return wasSuccessful ? 0 : 1; }
This file brings in the Text UI TestRunner from CppUnit. This test runner is a command-line text test runner that outputs dots to indicate test progress. It will print diagnostic messages if there are test failures. We also include the headers for the test files which we will explore later.
The main()
method creates the TestRunner
called runner
and proceeds to register test suites with the runner. We will see how to define the test suites later. Then we execute the run()
method of the test runner to execute the test suites.
Test Suite for IsPrime()
In the header file for the IsPrime test suite we create the IsPrimeTests
class that inherits from CppUnit::TestCase
. In addition to declaring all the test methods that we will create, we register all the test methods with the suite using the CPPUNIT_TEST()
macro. The CPPUNIT_TEST_SUITE(name)
and CPPUNIT_TEST_SUITE_END()
macros must bracket all the registration macros calls.
IsPrimeTests.h
#include <cppunit/TestCase.h> #include <cppunit/extensions/HelperMacros.h> class IsPrimeTests : public CppUnit::TestCase { CPPUNIT_TEST_SUITE(IsPrimeTests); CPPUNIT_TEST(testNegative); CPPUNIT_TEST(testTrivial); CPPUNIT_TEST(testMultiples); CPPUNIT_TEST(testPrimes); CPPUNIT_TEST(testComposite); CPPUNIT_TEST(testMersenne); CPPUNIT_TEST_SUITE_END(); public: void testNegative(); void testTrivial(); void testMultiples(); void testPrimes(); void testComposite(); void testMersenne(); };
To test this function we will use example-based test methods. Example-based tests are ones in which we know or can easily compute by hand the correct (expected) function results. We will define one method for each partition of the input space. We will make assertions using CppUnit macros that test to see if the condition under test is true. If the assertions fail, the test will fail and the failure will be reported by CppUnit.
For the first partition (negative numbers, 0 and 1) we define the test method as follows:
IsPrimeTests.cpp
/// Tests negative input, 0 and 1. void IsPrimeTests::testNegative() { CPPUNIT_ASSERT(!isPrime(1)); CPPUNIT_ASSERT(!isPrime(0)); CPPUNIT_ASSERT(!isPrime(-1)); CPPUNIT_ASSERT(!isPrime(-2)); CPPUNIT_ASSERT(!isPrime(INT_MIN)); CPPUNIT_ASSERT(!isPrime(INT64_MIN)); }
We specifically test 0 and 1 as they are called out explicitly in the partition definition. Then we select a handful of negative numbers including the values at the extreme ranges of our partition, -1 and INT64_MIN. All our examples should return false so we assert this with the CPPUNIT_ASSERT(!isPrime(x))
macro calls.
Next we define a test for the trivial partition of 2 and 3. Both these inputs are prime so we use the CPPUNIT_ASSERT(isPrime(x))
macro to assert this.
IsPrimeTests.cpp
/// Tests trivial cases 2 and 3 void IsPrimeTests::testTrivial() { CPPUNIT_ASSERT(isPrime(2)); CPPUNIT_ASSERT(isPrime(3)); }
The test for the third partition will check specific multiples of 2 and 3 to make sure that isPrime()
returns false.
IsPrimeTests.cpp
/// Tests multiples of 2 or 3. void IsPrimeTests::testMultiples() { CPPUNIT_ASSERT(!isPrime(4)); // x2 CPPUNIT_ASSERT(!isPrime(6)); // x2 x3 CPPUNIT_ASSERT(!isPrime(8)); // x2 CPPUNIT_ASSERT(!isPrime(9)); // x3 CPPUNIT_ASSERT(!isPrime(10)); // x2 CPPUNIT_ASSERT(!isPrime(12)); // x2 x3 CPPUNIT_ASSERT(!isPrime(14)); // x2 CPPUNIT_ASSERT(!isPrime(15)); // x3 }
For the fourth partition we will check a number of known primes to make sure that isPrime()
returns true.
IsPrimeTests.cpp
static const long primes[] = { 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 }; /// Test a number of larger primes void IsPrimeTests::testPrimes() { for (long x : primes) { CPPUNIT_ASSERT_MESSAGE(std::to_string(x) + " is not prime", isPrime(x)); } CPPUNIT_ASSERT(isPrime(INT32_MAX)); }
For the fifth partition we will check a number of known composite numbers to make sure that isPrime()
returns false. We will use the knowledge that primes (other than 2 and 3) can not be consecutive. So we can use the primes
list from before but just subtract one or add one to get a composite number (they will also be even which is why they are composite). When we use arrays of input data instead of a single datapoint in our asserts, we should use the CPPUNIT_ASSERT_MESSAGE(message, condition)
macro so that we can provide a message that will print the value of the datapoint if the assertion fails (condition is false). This helps with debugging the function and/or test if it fails.
IsPrimeTests.cpp
/// Test a number of larger composite numbers void IsPrimeTests::testComposite() { for (long x : primes) { CPPUNIT_ASSERT_MESSAGE(std::to_string(x - 1) + " is prime", !isPrime(x - 1)); CPPUNIT_ASSERT_MESSAGE(std::to_string(x + 1) + " is prime", !isPrime(x + 1)); } CPPUNIT_ASSERT(!isPrime(INT16_MAX)); CPPUNIT_ASSERT(!isPrime(INT64_MAX)); }
For one final check we will compute all the Mersenne numbers possible using the long
data type. Mersenne numbers are numbers of the form 2x - 1 where x is a positive integer. There are 63 Mersenne numbers that we can compute. Mersenne numbers are interesting because they can be prime but are not always prime. For example M2 = 22 - 1 = 3 is prime and M3 = 23 - 1 = 7 is prime but M4 = 24 - 1 = 15 is composite. In this test we test against a bitmapped value that indicates whether the Mn is prime by setting the nth bit in the value.
IsPrimeTests.cpp
/** * @brief Tests the primality of Mersenne numbers up to 2^63 - 1. * * This function verifies the primality of Mersenne numbers of the form M(x) = 2^x - 1 * for 2 <= x < 64. The expected primality of each Mersenne number is determined * by a predefined bitmap (`primalityBitmap`), where a bit is set if the corresponding * exponent x yields a prime Mersenne number. * * - For prime x, M(x) is a candidate for primality, but not all such numbers are prime. * - The bitmap (`primalityBitmap`) indicates which x values yield prime Mersenne numbers. * * @note Relies on the functions `ipow` for computing powers of 2 and `isPrime` for checking primality. * * @test Verifies that the `isPrime` function correctly identifies the primality of Mersenne numbers. * - If the corresponding bit in the bitmap is set, the number must be prime. * - Otherwise, it must not be prime. * * @throws Assertion failure if the primality test does not match the bitmap. * * @see isPrime * @see ipow */ void IsPrimeTests::testMersenne() { long primalityBitmap = 0x20000000800a20ac; for (int x = 2; x < 64; ++x) { long m = ipow(2, x) - 1; if (primalityBitmap & (1L << x)) { CPPUNIT_ASSERT_MESSAGE(std::to_string(m) + " is not prime", isPrime(m)); }else { CPPUNIT_ASSERT_MESSAGE(std::to_string(m) + " is prime", !isPrime(m)); } } }
With all these example-based tests, we can be fairly confident that the IsPrime()
function will work for all long
values because we have tested all the code paths in the function. When we run the tests we should see the following output:
> cppunit_test
......
OK (6 tests)
Process finished with exit code 0
If we had assertions that fail, they would cause the test to fail and the assertion that failed will be reported. For example:
> cppunit_test
...F.F..
!!!FAILURES!!!
Test Results:
Run: 6 Failures: 2 Errors: 0
1) test: IsPrimeTests::testMultiples (F) line: 35 /Users/rich/Desktop/Resources/C++/Articles/CppUnitExamples/test/IsPrimeTests.cpp
assertion failed
- Expression: isPrime(16)
2) test: IsPrimeTests::testPrimes (F) line: 53 /Users/rich/Desktop/Resources/C++/Articles/CppUnitExamples/test/IsPrimeTests.cpp
assertion failed
- Expression: isPrime(x)
- 1000 is not prime
Process finished with exit code 1
Test Suite for Factorial()
We can continue to add test suites for each of the functions in our library. The next function we want to test is a factorial method that memoizes previously computed factorials so that they don't need to be recomputed each time. We add the factorial()
function to the GlobalFunctions.cpp file
, register the FactorialTests
test suite class with our test runner and then define the test suite header file.
GlobalFunctions.cpp
static vector<double> memoizedFactorials = {1}; static std::recursive_mutex memoMutex; // Mutex to protect memoizedFactorials /** * @brief Computes the factorial of a given integer. * * This function calculates the factorial of a non-negative integer `n` * by multiplying all positive integers up to `n`. The result is returned * as a `long` integer. The function supports factorials up to 20! due to * limitations on the range of `long`. * * @param n The integer for which to compute the factorial. Must be in the range [0, 20]. * @return The factorial of `n` as a `long` integer. * @throws std::invalid_argument If `n` is negative or greater than 170, as the * result would exceed the range of `double`. */ double factorial(int n) { if (n < 0 || n > 170) { throw std::invalid_argument("Error: Can only compute factorials in range [0, 170]. n=" + to_string(n)); } // Lock the mutex to protect access to the shared resource std::lock_guard<std::recursive_mutex> lock(memoMutex); double result = 1; // Check if the factorial has already been computed if (n < memoizedFactorials.size()) { result = memoizedFactorials[n]; }else { // Compute and memoize missing factorials result = n * factorial(n - 1); memoizedFactorials.push_back(result); } return result; }
testrunner.cpp
#include "IsPrimeTests.h" #include "FactorialTests.h" int main() { CppUnit::TextUi::TestRunner runner; // Register the test suite runner.addTest(IsPrimeTests::suite()); runner.addTest(FactorialTests::suite()); // Run the tests bool wasSuccessful = runner.run(); // Return whether all tests passed or failed return wasSuccessful ? 0 : 1; }
FactorialTests.h
#include <cppunit/TestCase.h> #include <cppunit/extensions/HelperMacros.h> class FactorialTests : public CppUnit::TestCase { CPPUNIT_TEST_SUITE(FactorialTests); CPPUNIT_TEST(testNegative); CPPUNIT_TEST(testZero); CPPUNIT_TEST(testPositive); CPPUNIT_TEST(testThrows); CPPUNIT_TEST_SUITE_END(); public: void testNegative(); void testZero(); void testPositive(); void testThrows(); };
There are four different code paths corresponding to four partitions of the input value. These partitions are:
- Negative inputs should throw an
std::invalid_argument
exception. - Input of 0 should use the initial memoized result of 1.
- Positive inputs should return the correct factorial. Repeat inputs to test memoization.
- Inputs > 170 should throw an
std::invalid_argument
exception.
FactorialTests.cpp
#include "GlobalFunctions.h" #include "FactorialTests.h" // Tests factorial of negative numbers. void FactorialTests::testNegative() { CPPUNIT_ASSERT_THROW(factorial(-10), std::invalid_argument); CPPUNIT_ASSERT_THROW(factorial(-5), std::invalid_argument); CPPUNIT_ASSERT_THROW(factorial(-1), std::invalid_argument); } // Tests factorial of 0. void FactorialTests::testZero() { CPPUNIT_ASSERT_DOUBLES_EQUAL(1, factorial(0), 0.); } // Tests factorial of positive numbers. void FactorialTests::testPositive() { CPPUNIT_ASSERT_DOUBLES_EQUAL(1, factorial(1), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(2, factorial(2), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(6, factorial(3), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(24, factorial(4), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(120, factorial(5), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(720, factorial(6), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(3628800, factorial(10), 0.); // Should already be computed and saved CPPUNIT_ASSERT_DOUBLES_EQUAL(362880, factorial(9), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(40320, factorial(8), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(5040, factorial(7), 0.); // Now for some really big ones CPPUNIT_ASSERT_DOUBLES_EQUAL(2'432'902'008'176'640'000, factorial(20), 0.); CPPUNIT_ASSERT_DOUBLES_EQUAL(7.25741561530799e+306, factorial(170), 3 * ULP(7.25741561530799e+306)); } // Test invalid arguments to factorial() void FactorialTests::testThrows() { CPPUNIT_ASSERT_THROW(factorial(171), std::invalid_argument); CPPUNIT_ASSERT_THROW(factorial(1000), std::invalid_argument); }
The testNegative()
method uses a new CppUnit assertion macro CPPUNIT_ASSERT_THROW(condition, exception type)
. This assertion tests to see if the condition will throw an exception of the type specified. If the condition does not throw or throws the wrong type of exception, the assertion will fail the test. In our case we expect negative inputs to throw std::invalid_argument
exceptions.
The second test will check the case when the input is 0. This input has a memoized result of 1 preallocated in our memoization vector. It is a simple test to make sure that the vector is initialized correctly.
The third test checks positive inputs to see if they compute the correct factorial. This example-based test checks some of the easy to compute factorials and some large factorials. It also tests factorials that should have already been previously computed and memoized for fast retrieval.
The final test checks values > 170 to make sure that they throw the expected exception.
Comparing Doubles
Comparing doubles in tests can be a bit difficult. We generally can't compare any two doubles for strict equivalence due to the nature of the floating point values are stored (IEEE 754). Many decimal numbers cannot be represented exactly in this binary format (e.g., 0.1 or 0.2). Instead, they are approximated to the nearest representable value. Small differences due to rounding or representation errors can accumulate, making exact comparison unreliable. For example, 0.1 + 0.2
does not exactly equal 0.3
due to rounding errors in floating-point arithmetic as can be seen in this assert which will fail.
CPPUNIT_ASSERT_EQUAL(0.3, 0.1 + 0.2);
Instead we must use the CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta
) macro to see if two doubles are within a small delta of each other, i.e abs(expected - actual) < delta
. Generally you want to choose a delta value that is around expected / 1e15
because doubles
have a bit more than 16 decimal digits of accuracy. An even better way to determine delta
is to compute the ULP (Unit Last Place) for expected
. ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP value depends on the floating-point number’s magnitude because the precision decreases as the number increases. If we choose delta
to be a small multiple of ULP we can create a good assertion to test two doubles.
CPPUNIT_ASSERT_DOUBLES_EQUAL
(0.3, 0.1 + 0.2, 2 * ULP(0.3));
GlobalFunctions.cpp
/** * @brief Computing the Unit in the Last Place (ULP) for a double. * * This function computes the ULP (Unit in the Last Place) which measures the * distance between two adjacent representable floating-point numbers. The ULP * value depends on the floating-point number’s magnitude because the precision * decreases as the number increases. * * @param x double to use in computing ULP * @return ULP for x */ double ULP(double x) { // Find the next representable value after x double next = std::nextafter(x, std::numeric_limits<double>::infinity()); // Compute the difference, which is the ULP return next - x; }
Test Suite for newtonSqrt()
Testing the newtonSqrt()
function requires us to make extensive use of double
comparisons. The newtonSqrt()
function is a square root method that uses Newton's method to compute the result. In addition to example-based tests, we will introduce the concept of property-based tests.
GlobalMethods.cpp
/** * @brief Calculates the square root of a number using Newton's method. * * This function approximates the square root of a given number by using * the Newton-Raphson iterative method. It converges to a solution within * a specified tolerance. * * @param number The number for which to calculate the square root. * Must be non-negative. * @param maxIterations The maximum number of iterations to perform (optional). * Defaults to 1000. * @return The approximate square root of the given number. * @throws std::invalid_argument If the number is negative. */ double newtonSqrt(double number, int maxIterations) { if (number < 0.0) { throw std::invalid_argument("Error: Cannot compute square root of a negative number."); } if (number == 0.0) { return 0.0; } // Initial guess (can be number / 2 or any positive number) double guess = number / 2; int i; for (i = 0; i < maxIterations; ++i) { double nextGuess = 0.5 * (guess + number / guess); // Check for convergence if (std::abs(nextGuess - guess) <= ULP(nextGuess)) { return nextGuess; } guess = nextGuess; } return guess; // Returns the approximation after max iterations }
To test the newtonSqrt()
function we can start by partitioning the input space for the newtonSqrt()
function into negative inputs and non-negative inputs. For the negative input scenario, the newtonSqrt()
function will throw an exception. For the non-negative case it should return the correct square root value.
For the negative input partition we make use of the CPPUNIT_ASSERT_THROW()
macro as we have seen to make sure that the newtonSqrt()
method throws a std::illegal_argument
exception for a few examples.
For the non-negative partition we want to create one example-based method that will test a few key inputs such as 1 or 100, that we can easily hard code the expected value. We might also want to test certain boundary cases like 0.
For the second test method we are going to use a property-based test that uses a random number generator to test a large number of non-negative inputs. With property-based tests, we don't necessarily know the correct output for the function under test, but we do know some relationship to the input that must be invariant. For example, in our square root case we know that x = √x * √x
for all x
.
Property-based tests let us perform many more tests than we can usually do with example-based tests. Not only do such tests give us more confidence in the correctness of our code, but they also can potentially find edge cases that we missed or didn't get quite right.
To generate uniformly distributed random integer or doubles we can use a simple template function getRandomNumber(low, high)
that is defined in GlobalFunctions.h
. Each time we call this method we will get a different pseudo-randomly generated number.
SqrtTests.cpp
/// Test negative cases that should throw. void SqrtTests::testNewtonSqrtThrows() { CPPUNIT_ASSERT_THROW(newtonSqrt(-1), std::invalid_argument); CPPUNIT_ASSERT_THROW(newtonSqrt(-2), std::invalid_argument); CPPUNIT_ASSERT_THROW(newtonSqrt(-0.5), std::invalid_argument); } /// Test boundary cases and other easy cases. void SqrtTests::testNewtonSqrt() { CPPUNIT_ASSERT_EQUAL(0.0, newtonSqrt(0.0)); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.7071067811865476, newtonSqrt(0.5), 2 * ULP(0.707106781186548)); CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, newtonSqrt(1.0), 2 * ULP(1.0)); CPPUNIT_ASSERT_DOUBLES_EQUAL(1.4142135623730951, newtonSqrt(2.0), 2 * ULP(1.414213562373095)); CPPUNIT_ASSERT_DOUBLES_EQUAL(10.0, newtonSqrt(100.0), 2 * ULP(10.0)); } /// Property-based test of a large number of random values. /// The property that we will use is x == √x * √x void SqrtTests::testNewtonSqrtRand() { for (int i = 1; i <= 1000; i++) { double x = getRandomNumber(.001, 1.0); double y = newtonSqrt(x); CPPUNIT_ASSERT_DOUBLES_EQUAL(x, y * y, 2 * ULP(x)); } for (int i = 1; i <= 1000; i++) { double x = getRandomNumber(0, 100); double y = newtonSqrt(x); CPPUNIT_ASSERT_DOUBLES_EQUAL(x, y * y, 2 * ULP(x)); } for (int i = 1; i <= 1000; i++) { double x = getRandomNumber(100, 10'000); double y = newtonSqrt(x); CPPUNIT_ASSERT_DOUBLES_EQUAL(x, y * y, 2 * ULP(x)); } for (int i = 1; i <= 1000; i++) { double x = getRandomNumber(10'000, 1'000'000); double y = newtonSqrt(x); CPPUNIT_ASSERT_DOUBLES_EQUAL(x, y * y, 2 * ULP(x)); } for (int i = 1; i <= 1000; i++) { double x = getRandomNumber(1e6, 1e12); double y = newtonSqrt(x); CPPUNIT_ASSERT_DOUBLES_EQUAL(x, y * y, 2 * ULP(x)); } }
GlobalFunctions.h
/** * @brief Generates a random number within a specified range. * * This function generates a number in the range [low, high] for integral types * and [low, high) for floating point types using a uniform distribution. The * range is inclusive of `low` and inclusive of `high` for integral types and * exclusive of 'high' for floating point types. * * @param low The lower bound of the random number range. * @param high The upper bound of the random number range. * @return A random long integer between `low` and `high`. * @note Uses a random device for seeding to ensure randomness. */ template <typename T> T getRandomNumber(T low, T high) { // Seed the random number generator with a random device std::random_device rd; std::mt19937 gen(rd()); // Mersenne Twister engine // Select the appropriate distribution if constexpr (std::is_integral<T>::value) { std::uniform_int_distribution<T> dist(low, high); return dist(gen); } else if constexpr (std::is_floating_point<T>::value) { std::uniform_real_distribution<T> dist(low, high); return dist(gen); } }
Test Suite for ipow()
While it somewhat defeats the purpose of a good test to compute the expected values of outputs and compare them to the result of the function, if there is a property that you can take advantage of when computing outputs from previous outputs, it can be helpful to leverage that property. For example successive values of the factorial n!
can be computed from the previous factorial (n - 1)!
using the relationship:
n!
== n * (n - 1)!
If we test the output of the factorial using a sequence of inputs that differ by 1, we can compute the next expected output by multiplying n
by the previous output.
Another such function that has such a property that we can leverage is the ipow()
function. This function computes the result of a long
value raised to an int
power using a fast method called exponentiation-by-squaring. If we compute a power bn we can compare it to the previously computed power bn-1
with the relationship:
bn == b * bn-1
GlobalFunctions.cpp
/** * @brief Computes the power of an integer using exponentiation by squaring. * * This function calculates \( x^y \) (x raised to the power of y) using the * efficient exponentiation by squaring method, which has a time complexity * of \( O(\log y) \). It is particularly useful for computing large powers * in a short time. * * @param x The base, which is a long integer. * @param y The exponent, which is a non-negative integer. * @return The result of \( x^y \) as a long integer. * @note This function does not handle negative exponents. Both x and y * should be chosen such that the result does not exceed the range * of a long integer to avoid overflow. */ long ipow(long x, int y) { long result = 1; while (y > 0) { if (y % 2 == 1) { result *= x; } x *= x; y /= 2; } return result; }
This relationship allows us to start with a value b
and its first power b0
which is just 1. Then compute successive powers by just multiplying by b
. This will give us the expected value to use when testing the ipow()
function.
IPowTests.cpp
// Test a wide variety of inputs, computing the correct answer incrementally. void IPowTests::testIPow() { for (long x = -10; x <= 10; ++x) { long p = 1; for (int i = 0; i <= 18; ++i) { CPPUNIT_ASSERT_EQUAL(p, ipow(x, i)); p *= x; } } } // Test all the possible powers of 2, computing the correct answer incrementally. void IPowTests::testPowersOf2() { long p = 1; for (int i = 0; i <= 63; ++i) { CPPUNIT_ASSERT_EQUAL(p, ipow(2, i)); p *= 2; } }
Test Suite String Upper/Lower Tests
Another example of a property-based test is to check the output for certain patterns. This is helpful in when testing string functions such toUpperCase()
and toLowerCase()
. We can compose test functions that generate a large number of random strings s
. We then check the results of calls to toUpperCase(s)
to make sure the results contain no lowercase characters. We take the same approach to testing the results of toLowerCase(x)
to make sure that the results contain no uppercase characters.
CaseTests.cpp
/// Test toUpperCase() for correctness with example-based test. void CaseTests::testUpperCase() { CPPUNIT_ASSERT_EQUAL(string("ABCDEFG"), toUpperCase("AbCdEfG")); CPPUNIT_ASSERT_EQUAL(string("ABCDEFG"), toUpperCase("aBcDeFg")); // Doesn't work for non-ASCII characters CPPUNIT_ASSERT_EQUAL(string("CAFé"), toUpperCase("Café")); // Does work if we use e + Combining Accute Accent CPPUNIT_ASSERT_EQUAL(string("CAFE\u0301"), toUpperCase("CafE\u0301")); } /// Test toLowerCase() for correctness with example-based test. void CaseTests::testLowerCase() { CPPUNIT_ASSERT_EQUAL(string("abcdefg"), toLowerCase("AbCdEfG")); CPPUNIT_ASSERT_EQUAL(string("abcdefg"), toLowerCase("aBcDeFg")); // Doesn't work for non-ASCII characters CPPUNIT_ASSERT_EQUAL(string("cafÉ"), toLowerCase("CafÉ")); // Does work if we use E + Combining Accute Accent CPPUNIT_ASSERT_EQUAL(string("cafe\u0301"), toLowerCase("CafE\u0301")); } /// Property-based test with random inputs. void CaseTests::testRandom() { for (int i = 1; i <= 100; i++) { stringstream ss; for (int j = 0; j < getRandomNumber(10, 40); j++) { ss << (char)getRandomNumber(32, 126); } string s = toLowerCase(ss.str()); CPPUNIT_ASSERT_EQUAL(ss.str().length(), s.size()); for (auto c : s) { if (isupper(c)) CPPUNIT_FAIL("The string shouldn't contain an uppercase letter! toLowerCase(" + ss.str() + ")"); } s = toUpperCase(ss.str()); CPPUNIT_ASSERT_EQUAL(ss.str().length(), s.size()); for (auto c : s) { if (islower(c)) CPPUNIT_FAIL("The string shouldn't contain an lowercase letter! toUpperCase(" + ss.str() + ")"); } } }
In the property-based test, we decompose the outputs from the test functions into individual characters in a loop. We then check to make sure that the individual characters don't match upper/lower case as appropriate. If we do find a character that should have been converted but wasn't, we use the CPPUNIT_FAIL(message)
macro to cause the test to fail. We supply an argument to the macro that includes the input value so that we can perhaps add that input to the example-based test if this test fails. We can then debug the problem using the example-based test.
CppUnit Assertions
CppUnit provides a variety of assertion macros that allow you to verify conditions in your unit tests. These assertions help determine if the code under test behaves as expected. Here are the commonly used CppUnit assertion macros:
Basic Assertions
CPPUNIT_FAIL(message)
• Always causes the test to fail.
CPPUNIT_ASSERT(condition)
• Asserts that condition is true. If condition is false, the test fails.
• Example: CPPUNIT_ASSERT(x > 0);
CPPUNIT_ASSERT_MESSAGE(message, condition)
• Similar to CPPUNIT_ASSERT
, but allows you to provide a custom message if the assertion fails.
• Example: CPPUNIT_ASSERT_MESSAGE("x should be positive", x > 0);
Equality and Inequality Assertions
CPPUNIT_ASSERT_EQUAL(expected, actual)
• Asserts that expected is equal to actual. This is commonly used for comparing values.
• Example: CPPUNIT_ASSERT_EQUAL(326, x);
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, expected, actual)
• Similar to CPPUNIT_ASSERT_EQUAL
, but includes a custom failure message.
• Example: CPPUNIT_ASSERT_EQUAL_MESSAGE("x should be equal to 5", 5, x);
CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta)
• Asserts that two double values are equal within a given tolerance (delta).
• Example: CPPUNIT_ASSERT_DOUBLES_EQUAL(3.14159, pi, 0.00001);
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(message, expected, actual, delta)
• Similar to CPPUNIT_ASSERT_DOUBLES_EQUAL
, but with a custom message.
• Example: CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("pi should be close to 3.14159", 3.14159, pi, 1e-5);
NOTE: Due to rounding errors, it is very unlikely that two floating-point values will match exactly, so CPPUNIT_ASSERT_EQUALS()
is not suitable. In general, for floating-point comparison to make sense, the user needs to carefully choose the error bound. Due to these slight rounding errors when using floating point types, you should always use the CPPUNIT_ASSERT_DOUBLES_EQUAL() macro when comparing doubles and not CPPUNIT_ASSERT_EQUALS(). See Comparing Floating Point Numbers.
Null and Non-Null Assertions
CPPUNIT_ASSERT_NULL(pointer)
• Asserts that pointer is nullptr.
• Example: CPPUNIT_ASSERT_NULL(ptr);
CPPUNIT_ASSERT_NOT_NULL(pointer)
• Asserts that pointer is not nullptr.
• Example: CPPUNIT_ASSERT_NOT_NULL(ptr);
Exception Assertions
CPPUNIT_ASSERT_THROW(expression, exceptionType)
• Asserts that executing expression throws an exception of type exceptionType.
• Example: CPPUNIT_ASSERT_THROW(throwExceptionFunction(), std::runtime_error);
CPPUNIT_ASSERT_NO_THROW(expression)
• Asserts that executing expression does not throw any exception.
• Example: CPPUNIT_ASSERT_NO_THROW(safeFunction());
Range Assertions
CPPUNIT_ASSERT_LESS(a, b)
• Asserts that a is less than b.
• Example: CPPUNIT_ASSERT_LESS(x, 326);
CPPUNIT_ASSERT_GREATER(a, b)
• Asserts that a is greater than b.
• Example: CPPUNIT_ASSERT_GREATER(x, 326);
CPPUNIT_ASSERT_LESS_EQUAL(a, b)
• Asserts that a is less than or equal to b.
• Example: CPPUNIT_ASSERT_LESS_EQUAL(x, 326);
CPPUNIT_ASSERT_GREATER_EQUAL(a, b)
• Asserts that a is greater than or equal to b.
• Example: CPPUNIT_ASSERT_GREATER_EQUAL(x, 326);
Additional Notes
• Assertions in CppUnit are used within test cases (subclasses of CppUnit::TestCase
), and they form the core checks that determine whether a test passes or fails.
• Many of these assertions (such as CPPUNIT_ASSERT_EQUAL
) work with basic types like integers, strings, and doubles, while custom objects may require overloading the operator==
for comparison.