C++ unit tests

Some of the C++ code is covered by unit tests that test whether it works correctly. These tests can be found in tests/ subdirectories within src/. They use the Catch2 testing framework, which provides some useful macros for structuring tests and checking results. The best way to learn about this is to look at the existing tests’ source code and the Catch2 documentation.

Mocking

There is one somewhat non-standard aspect of these tests that deserves an explanation, and that’s the mocking system. When you’re testing a class or a function A that depends on another class or function B, you end up testing the whole combined collection of functions and classes. For a unit test, that’s not what you want.

The solution to this is called mocking, which means that you replace the real B by a different class or function that has the same interface as the real B (so that A can call it) but that records what A did, and feeds it a pre-set response. The recording in this mock class or mock function can then be checked after calling A to make sure everything worked as expected.

In a language like Python, this replacement is easy to do because it’s a dynamic language, meaning that functions and classes are just objects that can be replaced from the test code. In C++, functions and classes are combined together at compile time, and cannot be changed at runtime unless you’ve explicitly programmed it like this. As a result, we cannot just replace B with a mock B from the test code.

Dependency inversion

One way to solve this is by a mechanism called dependency inversion. Instead of writing

class B {
    public:
        g();
};

class A {
    public:
        f() { b_.g() };

    private:
        B b_;
};

we do

/* Interface for B */
class IB {
    public:
        g();
};

class B {
    public:
        g() { ... };
};

class A {
    public:
        A(shared_ptr<IB> b) : b_(b) {};

        f() { b_->g() };

    private:
        IB b_;
};

Now we can put a real B into A when we create it in the real code, and a mock B (which also implements IB) in the test code.

As you can see however, this means writing a lot more code because now there are these interfaces everywhere, and you need to make separate factory classes to make all the objects and wire them together, and it all gets very complicated, just to be able to write a unit test! This is the normal approach in Java, and one of the reasons that Java programs tends to have so much boilerplate.

Preprocessor-based mocking

In C++, there’s another way of doing mocks that avoids all this extra abstraction, and that is to use the preprocessor. If your C++ code is nicely organised, then every class is declared in a header (e.g. class.hpp) and defined in a corresponding source file (class.cpp). To compile the program, each source file is compiled, and then they are all linked together into a single executable. To use one class from another class, you #include the header, which contains all the information needed to use the class.

Now it would be nice if, when building the test, we could just compile as usual except using a mock_class.cpp that contains a mock implementation. However, that’s unlikely to work because the mock probably needs different or at least some extra member variables to store information on how it was called, and those member variables are in the header. So the header needs to be replaced too.

To make this possible, we make a small modification to b.hpp to make it look like this:

#ifdef _MOCK_B_HPP_
#include _MOCK_B_HPP_
#else

class B {
    public:
        g() { ... };
};

#endif

When we’re compiling the model normally, _MOCK_B_HPP_ is not defined, and B works normally. When we’re compiling the test, we set _MOCK_B_HPP_ to the name of a header file that declares a mock class B with a compatible interface, but with test logic inside of it. The real A will then be compiled against the mock B, and linked to the mock B as well, after which the test can make an A and ask the mock B what A did.

The tests are not actually built one file at a time. Instead, the test itself is a program defined in a .cpp file which includes all the other necessary code directly. This is somewhat of an ugly hack, and there are corner cases for which it just plain doesn’t work, but the alternative is a very hacky Makefile to build it all and that’s not great either.

If you look at the tests, for example src/adhesions/tests/test_adhesion_mover.cpp, you’ll see the following pattern:

// Tell the preprocessor to replace some real files with mocks
#define _MOCK_ADHESION_INDEX_HPP_ "mock_adhesion_index.hpp"
...

// Now load the real implementations, which will now use the mocks
#include "adhesion_mover.cpp"
...

// And add the mock implementations
#include "mock_adhesion_index.cpp"
...

When this file is compiled, the preprocessor will first run into the definition of _MOCK_ADHESION_INDEX_HPP_, which it sets. Then it goes and loads adhesion_mover.cpp, which includes adhesion_index.hpp, so it loads that as well. However, because _MOCK_ADHESION_INDEX_HPP_ has been set, instead of taking the real AdhesionIndex class, it loads mock_adhesion_index.hpp which contains the mock declaration. Finally, it’ll include the mock’s implementation, so that we get this:

// begin adhesion_mover.cpp

    // begin adhesion_mover.hpp
        // real AdhesionMover declaration
    // end adhesion_mover.hpp

    // begin adhesion_index.hpp

        // begin mock_adhesion_index.hpp
            // mock AdhesionIndex declaration
        // end mock_adhesion_index.hpp

        // declaration of real AdhesionIndex omitted
    // end adhesion_index.hpp

    // real AdhesionMover implementation
// end adhesion_mover.cpp

// begin mock_adhesion_index.cpp
    // mock AdhesionIndex implementation
// end mock_adhesion_index.cpp

As you can see, we now have the real AdhesionMover declaration and definition, and the mock AdhesionIndex, with only four extra lines added to adhesion_index.hpp and no other changes to the code.

Including source files (as opposed to headers) directly is pretty much always evidence of something having gone seriously wrong, but in this case I think it’s justifiable. The alternative would be a very complex make target that needs to be carefully kept in sync with the test. Including everything means that there’s a nice overview at the top of the file of what is real code under test, and what has been mocked, and that is easy to modify as needed as tests are added. Things will break if you have two .cpp files that declare a local symbol with the same name, but we don’t have any of that here so it all works fine.