Chapter 10 Exercise Set 2: Introducing DOCTEST and TDD

doctest is a software framework for unit testing in C++. Since it is light weight, comparatively easy to use, and free software, we will begin using it for exercises for the remainder of the book.

Appendix C: A development environment for unit testing has instructions for configuring your computer for using doctest.

For the rest of this exercise set you will be guided in developing a small library of utilities for processing vectors of integers. We will use doctest to do this with a test-driven development approach.

We will also need to sneak in a preview of header files, which will be discussed more fully in the Header files section of the next chapter.

We’ll begin with a file named test_num_vector_utils.cpp containing:

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
#include <string>
#include <vector>
#include "num_vector_utils.h"
using namespace std;

TEST_CASE("Testing render_num_vector") {
    vector<int> nums = {1, 3, 7};
    string numstr = render_num_vector(nums);
    string expected = "1 3 7";
    CHECK(numstr == expected);
}

We create a test case that describes what we want the render_num_vector function do before we start trying to make it do it. Learning to thinking about what you want to happen with your function and expressing it in calls to the function together with expected output can help make you a more effective programmer.

Here we are stating that given a vector of integers initialized with {1, 3, 7}, we want our render_num_vector function to turn that into a string equal to "1 3 7".

In for the C++ compiler to be able to compile this program, it needs to know the function prototype for the render_num_vector function, but it does not need to know its function definition (i.e. the body of the function).

We put this function prototype in a header file named num_vector_utils.h that looks like this:

#include <string>
#include <vector>
using namespace std;

string render_num_vector(const vector<int>&);

At this stage we only want to define the function in a way that will allow it to compile without syntax errors. We are not yet concerned with making the test pass. In fact, we want it to fail at this stage, since in test-driven development it is a failing test that drives us forward.

This num_vector_utils.cpp will do the trick:

#include <string>
#include <vector>
using namespace std;

string render_num_vector(const vector<int>&) {
    string s = "";
    return s;
}

With all three of these files in the same directory, you should now be able to compile the test program with:

$ g++ test_num_vector_utils.cpp num_vector_utils.cpp

It should compile without error, and running it should give you:

[doctest] doctest version is "2.4.11"
[doctest] run with "--help" for options
===============================================================================
test_render_num_vector.cpp:8:
TEST CASE:  Testing render_num_vector

test_render_num_vector.cpp:12: ERROR: CHECK( numstr == expected ) is NOT correct!
  values: CHECK(  == 1 3 7 )

===============================================================================
[doctest] test cases: 1 | 0 passed | 1 failed | 0 skipped
[doctest] assertions: 1 | 0 passed | 1 failed |
[doctest] Status: FAILURE!
  1. Change the function definition (the body) of render_num_vector so that this first test passed.

  2. Add at least two more tests to better test your function. Does it work with vectors that contain other than three elements? What should it return when you pass it an empty vector?

  3. Add the following test case and the minimal code to the header and source files for the num_vector_sum function needed to make the the test compile (with a failing test!). Then add a body to the function which will make the test pass. Finally, add additional CHECKs to the test case and confirm that these also pass.

    TEST_CASE("Testing num_vector_sum") {
        vector<int> nums = {1, 3, 7};
        CHECK(11 == num_vector_sum(nums));
    }
    
  4. Add scaffolding (the minimum code needed to make the test compile and and fail) for a function named num_vector_product. Then add a body to it that will make the test pass, and additional checks to the test case to make it more robust.

    TEST_CASE("Testing num_vector_product") {
        vector<int> nums = {2, 3, 7};
        CHECK(42 == num_vector_product(nums));
    }
    
  5. Add a function named only_evens and use the following test case as a guide to implementing it.

    TEST_CASE("Testing only_evens") {
        vector<int> nums = {1, 2, 3, 4, 6, 7, 8, 11, 12, 14, 27, 22};
        vector<int> evens = only_evens(nums);
        string expected = "2 4 6 8 12 14 22";
        CHECK(render_num_vector(evens) == expected);
    }
    
  6. Add a test case for a function named only_odds that works anologously for the odd numbers in the vector as the prevous exercise did for evens. Then continue the process of writing scaffolding code to make the tests compile (and fail), and then finally add a function body to make the tests pass.

  7. Add a function named nums_between(nums, low, high) and use the following test case as a guide to implementing it.

    TEST_CASE("Testing nums_between function") {
        vector<int> nums = {11, 2, 13, 4, 10, 26, 7, 88, 19, 20, 14, 5, 32};
        vector<int> nums2 = nums_between(nums, 10, 20);
        string expected = "11 13 10 19 20 14";
        CHECK(render_num_vector(nums2) == expected);
    }
    
  8. Add a function named mean that returns a double with the value of the arithmetic mean of the numbers in a number list. You know the drill by now, start by writing a test, then write the scaffolding to make the test compile (and fail), and then finally write a function body to make the test pass.

Note

The next two exercises are significantly more challenging than the previous ones. It would be very challenging indeed to write solutions using only the algorithmic tools we have seen so far. To help you find more managable solutions, take a look at the algorithms library, in particular the sort and count functions. Using these functions should greatly help you complete these exercises.

  1. Repeat the process you used in the previous exercise to implement a function median that computes the median of the numbers in a number vector.

  2. Add a function mode that returns the statistical mode of the numbers in a number vector. Since the set of numbers may be either unimodal or multimodal, this function should return a vector of integers with one or more elements.