C++ Unit Testing with GoogleTest

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 GoogleTest for use with C++ code. You can find GoogleTest at the website https://github.com/google/googletest. Click on the "Code" button and download the ZIP version or click on the "Releases" link to get the latest stable release ZIP file.

To setup GoogleTest for use in just one project, expand the googletest-1.15.x.zip file into a folder named "googletest-1.15.x" in your "your_project_name" folder. Then execute the following commands:

> cd your_project_name/googletest-1.15.x
> cmake .
> make

After you build GoogleTestGoogleTest, add the following lines to your project's cmake CMakeLists.txt file:

include_directories(googletest-1.15.2/googletest/include)
link_directories(googletest-1.15.2/lib)
target_link_libraries(<your_project_name> gtest gtest_main)

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

GoogleTest provides a test runner main() method to act as the initiating method for running the tests. It is in the gtest_main library that we link into our test executable as specified in our CMakeLists.txt file. Most modern IDEs support GoogleTest and will provide a special run option to run the tests defined in your project.

Test Suite for IsPrime()

We don't need any header files to that register our tests with GoogleTest as our tests are automatically registered if we use the proper GoogleTest macros to define our tests.

To test IsPrime() 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 GoogleTest 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 GoogleTest.

For the first partition (negative numbers, 0 and 1) we define the test method as follows:

SimpleFunctionTests.cpp
/// Tests negative input, 0 and 1.
TEST(isPrimeTest, Negative) {
    EXPECT_FALSE(isPrime(1));
    EXPECT_FALSE(isPrime(0));
    EXPECT_FALSE(isPrime(-1));
    EXPECT_FALSE(isPrime(-2));
    EXPECT_FALSE(isPrime(INT_MIN));
    EXPECT_FALSE(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 EXPECT_FALSE(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 EXPECT_TRUE(isPrime(x)) macro to assert this.

SimpleFunctionTests.cpp
/// Tests trivial cases 2 and 3
 TEST(isPrimeTest, Trivial) {
    EXPECT_TRUE(isPrime(2));
    EXPECT_TRUE(isPrime(3));
}

The test for the third partition will check specific multiples of 2 and 3 to make sure that isPrime() returns false.

SimpleFunctionTests.cpp
/// Tests multiples of 2 or 3.
TEST(isPrimeTest, Multiples) {
    EXPECT_FALSE(isPrime(4));    // x2
    EXPECT_FALSE(isPrime(6));    // x2 x3
    EXPECT_FALSE(isPrime(8));    // x2
    EXPECT_FALSE(isPrime(9));    // x3
    EXPECT_FALSE(isPrime(10));   // x2
    EXPECT_FALSE(isPrime(12));   // x2 x3
    EXPECT_FALSE(isPrime(14));   // x2
    EXPECT_FALSE(isPrime(15));   // x3
}

For the fourth partition we will check a number of known primes to make sure that isPrime() returns true.

SimpleFunctionTests.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
TEST(isPrimeTest, Primes) {
    for (long x : primes) {
        EXPECT_TRUE(isPrime(x)) << x << " is not prime";
    }
    EXPECT_TRUE(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

EXPECT_FALSE(condition) << message;

form of the macro so that we can provide a message that will print the value of the datapoint if the assertion fails (condition is false). You can use the put-to operator (<<) to add a detailed message to any EXPECT_xxx() macro. This helps with debugging the function and/or test if it fails.

SimpleFunctionTests.cpp
/// Test a number of larger composite numbers
TEST(isPrimeTest, Composite) {
    for (long x : primes) {
        EXPECT_FALSE(isPrime(x - 1)) << (x - 1) << " is prime";
        EXPECT_FALSE(isPrime(x + 1)) << (x + 1) << " is prime";
    }
    EXPECT_FALSE(isPrime(INT16_MAX));
    EXPECT_FALSE(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.

SimpleFunctionTests.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
 */
TEST(isPrimeTest, Mersenne) {
    // Check Mersenne numbers.
    long primalityBitmap = 0x20000000800a20ac;
    for (int x = 2; x < 64; ++x) {
        long m = ipow(2, x) - 1;
        if (primalityBitmap & (1L << x)) {
            EXPECT_TRUE(isPrime(m)) << m << " is not prime";
        }else {
            EXPECT_FALSE(isPrime(m)) << m << " is prime";
        }
    }
}

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:

> google_test
[----------] 6 tests from isPrimeTest
[ RUN ] isPrimeTest.Negative
[ OK ] isPrimeTest.Negative (0 ms)
[ RUN ] isPrimeTest.Trivial
[ OK ] isPrimeTest.Trivial (0 ms)
[ RUN ] isPrimeTest.Multiples
[ OK ] isPrimeTest.Multiples (0 ms)
[ RUN ] isPrimeTest.Primes
[ OK ] isPrimeTest.Primes (0 ms)
[ RUN ] isPrimeTest.Composite
[ OK ] isPrimeTest.Composite (0 ms)
[ RUN ] isPrimeTest.Mersenne
[ OK ] isPrimeTest.Mersenne (555 ms)
[----------] 6 tests from isPrimeTest (555 ms total)

If we had assertions that fail, they would cause the test to fail and the assertion that failed will be reported. For example:

> google_test
[----------] 6 tests from isPrimeTest
[ RUN ] isPrimeTest.Negative
[ OK ] isPrimeTest.Negative (0 ms)
[ RUN ] isPrimeTest.Trivial
[ OK ] isPrimeTest.Trivial (0 ms)
[ RUN ] isPrimeTest.Multiples
/Users/rich/Desktop/Resources/C++/Articles/GoogleTestExamples/test/SimpleFunctionTests.cpp:58: Failure
Value of: isPrime(16)
Actual: false
Expected: true

[ FAILED ] isPrimeTest.Multiples (0 ms)
[ RUN ] isPrimeTest.Primes
/Users/rich/Desktop/Resources/C++/Articles/GoogleTestExamples/test/SimpleFunctionTests.cpp:76: Failure
Value of: isPrime(x)
Actual: false
Expected: true
1000 is not prime

[ FAILED ] isPrimeTest.Primes (0 ms)
[ RUN ] isPrimeTest.Composite
[ OK ] isPrimeTest.Composite (0 ms)
[ RUN ] isPrimeTest.Mersenne
[ OK ] isPrimeTest.Mersenne (560 ms)
[----------] 6 tests from isPrimeTest (560 ms total)

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;
}

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.
SimpleFunctionTests.cpp
// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) {
    EXPECT_THROW(factorial(-10), std::invalid_argument);
    EXPECT_THROW(factorial(-5), std::invalid_argument);
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}

// Tests factorial of 0.
TEST(FactorialTest, Zero) { EXPECT_DOUBLE_EQ(1, factorial(0)); }

// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
    EXPECT_DOUBLE_EQ(1, factorial(1));
    EXPECT_DOUBLE_EQ(2, factorial(2));
    EXPECT_DOUBLE_EQ(6, factorial(3));
    EXPECT_DOUBLE_EQ(24, factorial(4));
    EXPECT_DOUBLE_EQ(120, factorial(5));
    EXPECT_DOUBLE_EQ(720, factorial(6));
    EXPECT_DOUBLE_EQ(3628800, factorial(10));
    // Should already be computed and saved
    EXPECT_DOUBLE_EQ(362880, factorial(9));
    EXPECT_DOUBLE_EQ(40320, factorial(8));
    EXPECT_DOUBLE_EQ(5040, factorial(7));
    // Now for some really big ones
    EXPECT_DOUBLE_EQ(2'432'902'008'176'640'000, factorial(20));
    EXPECT_NEAR(7.25741561530799e+306, factorial(170), 3 * ULP(7.25741561530799e+306));
}

// Test invalid arguments to factorial()
TEST(FactorialTest, Invalid) {
    EXPECT_THROW(factorial(171), std::invalid_argument);
    EXPECT_THROW(factorial(1000), std::invalid_argument);
}

The testNegative() method uses a new GoogleTest assertion macro EXPECT_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.

EXPECT_EQ(0.3, 0.1 + 0.2);

Instead we must use the EXPECT_NEAR(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.

EXPECT_NEAR(0.3, 0.1 + 0.2, 2 * ULP(0.3));

GoogleTest has another convenience macro for comparing doubles that always uses 4 * ULP() as the delta value.

EXPECT_DOUBLE_EQ(0.3, 0.1 + 0.2);

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 EXPECT_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.

SimpleFunctionTests.cpp
/// Test negative cases that should throw.
TEST(SqrtTest, Throws) {
    EXPECT_THROW(newtonSqrt(-1), std::invalid_argument);
    EXPECT_THROW(newtonSqrt(-2), std::invalid_argument);
    EXPECT_THROW(newtonSqrt(-0.5), std::invalid_argument);
}

/// Test boundary cases and other easy cases.
TEST(SqrtTest, NewtonSqrt) {
    EXPECT_EQ(0.0, newtonSqrt(0.0));
    EXPECT_NEAR(0.7071067811865476, newtonSqrt(0.5), 2 * ULP(0.707106781186548));
    EXPECT_NEAR(1.0, newtonSqrt(1.0), 2 * ULP(1.0));
    EXPECT_NEAR(1.4142135623730951, newtonSqrt(2.0), 2 * ULP(1.414213562373095));
    EXPECT_NEAR(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
TEST(SqrtTest, NewtonSqrtRand) {
    for (int i = 1; i <= 1000; i++) {
        double x = getRandomNumber(.001, 1.0);
        double y = newtonSqrt(x);
        EXPECT_NEAR(x, y * y, 2 * ULP(x));
    }
    for (int i = 1; i <= 1000; i++) {
        double x = getRandomNumber(0, 100);
        double y = newtonSqrt(x);
        EXPECT_NEAR(x, y * y, 2 * ULP(x));
    }
    for (int i = 1; i <= 1000; i++) {
        double x = getRandomNumber(100, 10'000);
        double y = newtonSqrt(x);
        EXPECT_NEAR(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);
        EXPECT_NEAR(x, y * y, 2 * ULP(x));
    }
    for (int i = 1; i <= 1000; i++) {
        double x = getRandomNumber(1e6, 1e12);
        double y = newtonSqrt(x);
        EXPECT_NEAR(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.

SimpleFunctionTests.cpp
// Test a wide variety of inputs, computing the correct answer incrementally.
TEST(IPowTest, exhaustive) {
    for (long x = -10; x <= 10; ++x) {
        long p = 1;
        for (int i = 0; i <= 18; ++i) {
            EXPECT_EQ(p, ipow(x, i));
            p *= x;
        }
    }
}

// Test all the possible powers of 2, computing the correct answer incrementally.
TEST(IPowTest, powersOf2) {
    long p = 1;
    for (int i = 0; i <= 63; ++i) {
        EXPECT_EQ(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.

SimpleFunctionTests.cpp
/// Test toUpperCase() for correctness with example-based test.
TEST(CaseTests, UpperCase) {
    EXPECT_EQ(string("ABCDEFG"), toUpperCase("AbCdEfG"));
    EXPECT_EQ(string("ABCDEFG"), toUpperCase("aBcDeFg"));
    // Doesn't work for non-ASCII characters
    EXPECT_EQ(string("CAFé"), toUpperCase("Café"));
    // Does work if we use e + Combining Accute Accent
    EXPECT_EQ(string("CAFE\u0301"), toUpperCase("CafE\u0301"));
}

/// Test toLowerCase() for correctness with example-based test.
TEST(CaseTests, LowerCase) {
    EXPECT_EQ(string("abcdefg"), toLowerCase("AbCdEfG"));
    EXPECT_EQ(string("abcdefg"), toLowerCase("aBcDeFg"));
    // Doesn't work for non-ASCII characters
    EXPECT_EQ(string("cafÉ"), toLowerCase("CafÉ"));
    // Does work if we use E + Combining Accute Accent
    EXPECT_EQ(string("cafe\u0301"), toLowerCase("CafE\u0301"));
}

/// Property-based test with random inputs.
TEST(CaseTests, Random) {
    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());
        EXPECT_EQ(ss.str().length(), s.size());
        for (auto c : s) {
            if (isupper(c))
                FAIL() << "The string shouldn't contain an uppercase letter! toLowerCase(" << ss.str() << ")";
        }
        s = toUpperCase(ss.str());
        EXPECT_EQ(ss.str().length(), s.size());
        for (auto c : s) {
            if (islower(c))
                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 FAIL() macro to cause the test to fail. We supply an message 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.

Test Fixtures

In addition to simple test functions, GoogleTest also allows you to create more complex test fixtures.

Test fixtures are classes that inherit from testing::Test. They allow us to define resources that can be shared by all the test methods. They also allow us to automatically perform setup and teardown operations before each test is run.

The test fixture class defines the shared resources as static members. Typically they would also be declared const so that they can't be changed by the test methods. After the test fixture class is defined we define all the test methods for the fixtures. To create test fixture test methods we use the TEST_F() macro. This macro takes the name of the text fixture class as the first argument and the name of the method as the second. For example, we can write a test fixture to test our isPrime() method from the previous post.

SimpleTestFixture.cpp
#include "GlobalFunctions.h"
#include "gtest/gtest.h"

class IsPrimeTestFixture : public testing::Test {
// You should make the members protected so that they can be
// accessed from sub-classes generated by TEST_F() macros.
protected:
    static constexpr long testPrimes[] = {
        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,
        1000000007, 1000000009, 1000000021, 1000000033, 1000000087, 1000000093, 1000000097, 1000000103, 1000000123,
        100000000003, 100000000019, 100000000057, 100000000063, 100000000069, 100000000073, 100000000091, 100000000103
    };
};

// When you have a test fixture, you define a test using TEST_F
// instead of TEST.

// Example-based test of known primes
TEST_F(IsPrimeTestFixture, PrimeExamples) {
    // You can access data in the test fixture here.
    for (auto x : testPrimes) {
        ASSERT_TRUE(isPrime(x));
    }
}

// Example/Property-based test that no primes other than 2 & 3 are consecutive.
TEST_F(IsPrimeTestFixture, CompositeExamples) {
    // You can access data in the test fixture here.
    for (auto x : testPrimes) {
        ASSERT_FALSE(isPrime(x - 1));
        ASSERT_FALSE(isPrime(x + 1));
    }
}

You can also define methods that will be run before and after each test is run. These are the Setup() and TearDown() methods. These methods can be used to make sure shared resources are always in the same state for each test method. In the next method we will test the std::vector<> class. We will setup a dynamically allocated vector in the Setup() method and destroy it in the TearDown() method.

ComplexTextFixture.cpp
#include <vector>

#include "gtest/gtest.h"

// Test fixture for the vector class.
class VectorTestFixture : public testing::Test {
// You should make the members protected so that they can be
// accessed from sub-classes generated by TEST_F() macros.
protected:
    std::vector<int> *numbers;

    // virtual void SetUp() will be called before each test is run.  You
    // should define it if you need to initialize the variables.
    // Otherwise, this can be skipped.
    void SetUp() override {
        numbers = new std::vector<int>{2, 3, 5};
    }

    // virtual void TearDown() will be called after each test is run.
    // You should define it if there is cleanup work to do.  Otherwise,
    // you don't have to provide it.
    //
    void TearDown() override {
        delete numbers;
    }
};

// When you have a test fixture, you define a test using TEST_F
// instead of TEST.

TEST_F(VectorTestFixture, Size) {
    ASSERT_EQ(3, numbers->size());
    numbers->push_back(7);
    ASSERT_EQ(4, numbers->size());
    numbers->pop_back();
    ASSERT_EQ(3, numbers->size());
}

TEST_F(VectorTestFixture, Push) {
    numbers->push_back(7);
    ASSERT_EQ(7, numbers->back());
    numbers->push_back(11);
    ASSERT_EQ(11, numbers->back());
    numbers->push_back(13);
    ASSERT_EQ(13, numbers->back());
}

TEST_F(VectorTestFixture, Pop) {
    ASSERT_EQ(5, numbers->back());
    numbers->pop_back();
    ASSERT_EQ(3, numbers->back());
    numbers->pop_back();
    ASSERT_EQ(2, numbers->back());
    numbers->pop_back();
}

TEST_F(VectorTestFixture, Empty) {
    ASSERT_FALSE(numbers->empty());
    numbers->pop_back();
    ASSERT_FALSE(numbers->empty());
    numbers->pop_back();
    ASSERT_FALSE(numbers->empty());
    numbers->pop_back();
    ASSERT_TRUE(numbers->empty());
}

For more information on all the available features of GoogleTest see https://google.github.io/googletest.

GoogleTest Assertions

GoogleTest 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 GoogleTest assertion macros:

Explicit Failure

FAIL

FAIL()

Generates a fatal failure, which returns from the current function.

Can only be used in functions that return void.

ADD_FAILURE

ADD_FAILURE()

Generates a non-fatal failure, which allows the current function to continue running.

ADD_FAILURE_AT

ADD_FAILURE_AT(file_path,line_number)

Generates a non-fatal failure at the file and line number specified.

Boolean Conditions

The following assertions test Boolean conditions.

EXPECT_TRUE

EXPECT_TRUE(condition)
ASSERT_TRUE(condition)

Verifies that condition is true.

EXPECT_FALSE

EXPECT_FALSE(condition)
ASSERT_FALSE(condition)

Verifies that condition is false.

Binary Comparison

The following assertions compare two values. The value arguments must be comparable by the assertion’s comparison operator, otherwise a compiler error will result.

If an argument supports the << operator, it will be called to print the argument when the assertion fails. Otherwise, GoogleTest will attempt to print them in the best way it can.

Arguments are always evaluated exactly once, so arguments can have side effects. However, the argument evaluation order is undefined and programs should not depend on any particular argument evaluation order.

These assertions work with both narrow and wide string objects (string and wstring).

EXPECT_EQ

EXPECT_EQ(val1,val2)
ASSERT_EQ(val1,val2)

Verifies that val1==val2.

Checks for equality of types that have equivalence operator defined. Does pointer equality on pointers. If used on two C strings, it tests if they are in the same memory location, not if they have the same value. Use EXPECT_STREQ() to compare C strings (e.g. const char*) by value.

When comparing a pointer to NULL, use EXPECT_EQ(ptr, nullptr) instead of EXPECT_EQ(ptr, NULL).

EXPECT_NE

EXPECT_NE(val1,val2)
ASSERT_NE(val1,val2)

Verifies that val1!=val2.

Checks for not equality of types that have equivalence operator defined. Does pointer equality on pointers. If used on two C strings, it tests if they are in different memory locations, not if they have different values. Use EXPECT_STRNE to compare C strings (e.g. const char*) by value.

When comparing a pointer to NULL, use EXPECT_NE(ptr, nullptr) instead of EXPECT_NE(ptr, NULL).

EXPECT_LT

EXPECT_LT(val1,val2)
ASSERT_LT(val1,val2)

Verifies that val1<val2.

EXPECT_LE

EXPECT_LE(val1,val2)
ASSERT_LE(val1,val2)

Verifies that val1<=val2.

EXPECT_GT

EXPECT_GT(val1,val2)
ASSERT_GT(val1,val2)

Verifies that val1>val2.

EXPECT_GE

EXPECT_GE(val1,val2)
ASSERT_GE(val1,val2)

Verifies that val1>=val2.

String Comparison

The following assertions compare two C strings. To compare two string objects, use EXPECT_EQ or EXPECT_NE instead.

These assertions also accept wide C strings (wchar_t*). If a comparison of two wide strings fails, their values will be printed as UTF-8 narrow strings.

To compare a C string with NULL, use EXPECT_EQ(c_string, nullptr) or EXPECT_NE(c_string, nullptr).

EXPECT_STREQ

EXPECT_STREQ(str1,str2)
ASSERT_STREQ(str1,str2)

Verifies that the two C strings str1 and str2 have the same contents.

EXPECT_STRNE

EXPECT_STRNE(str1,str2)
ASSERT_STRNE(str1,str2)

Verifies that the two C strings str1 and str2 have different contents.

EXPECT_STRCASEEQ

EXPECT_STRCASEEQ(str1,str2)
ASSERT_STRCASEEQ(str1,str2)

Verifies that the two C strings str1 and str2 have the same contents, ignoring case.

EXPECT_STRCASENE

EXPECT_STRCASENE(str1,str2)
ASSERT_STRCASENE(str1,str2)

Verifies that the two C strings str1 and str2 have different contents, ignoring case.

Floating-Point Comparison

The following assertions compare two floating-point values.

Due to rounding errors, it is very unlikely that two floating-point values will match exactly, so EXPECT_EQ is not suitable. In general, for floating-point comparison to make sense, the user needs to carefully choose the error bound.

GoogleTest also provides assertions that use a default error bound based on Units in the Last Place (ULPs). To learn more about ULPs, see the article Comparing Floating Point Numbers.

EXPECT_FLOAT_EQ

EXPECT_FLOAT_EQ(val1,val2)
ASSERT_FLOAT_EQ(val1,val2)

Verifies that the two float values val1 and val2 are approximately equal, to within 4 ULPs from each other. Infinity and the largest finite float value are considered to be one ULP apart.

EXPECT_DOUBLE_EQ

EXPECT_DOUBLE_EQ(val1,val2)
ASSERT_DOUBLE_EQ(val1,val2)

Verifies that the two double values val1 and val2 are approximately equal, to within 4 ULPs from each other. Infinity and the largest finite double value are considered to be one ULP apart.

EXPECT_NEAR

EXPECT_NEAR(val1,val2,abs_error)
ASSERT_NEAR(val1,val2,abs_error)

Verifies that the difference between val1 and val2 does not exceed the absolute error bound abs_error.

If val and val2 are both infinity of the same sign, the difference is considered to be 0. Otherwise, if either value is infinity, the difference is considered to be infinity. All non-NaN values (including infinity) are considered to not exceed an abs_error of infinity.

Exception Assertions

The following assertions verify that a piece of code throws, or does not throw, an exception. Usage requires exceptions to be enabled in the build environment.

Note that the piece of code under test can be a compound statement, for example:

EXPECT_NO_THROW({
int n = 5;
DoSomething(&n);
});

EXPECT_THROW

EXPECT_THROW(statement,exception_type)
ASSERT_THROW(statement,exception_type)

Verifies that statement throws an exception of type exception_type.

EXPECT_ANY_THROW

EXPECT_ANY_THROW(statement)
ASSERT_ANY_THROW(statement)

Verifies that statement throws an exception of any type.

EXPECT_NO_THROW

EXPECT_NO_THROW(statement)
ASSERT_NO_THROW(statement)

Verifies that statement does not throw any exception.

See https://google.github.io/googletest/reference/assertions.html for the complete GoogleTest assertions reference.