How can we use C++ to write tests?
Calling the test directly might not seem like a big problem right now because we only have one test. However, as more tests are added, the need to call each one from main
will lead to problems. Do you really want to have to modify the main
function every time you add or remove a test?
The C++ language doesn’t have a way to add extra custom information to a function or a class that could be used to identify all the tests. So, there is no way to look through all the code, find all the tests automatically, and run them.
One of the tenets of C++ is to avoid adding language features that you might not need, especially language features that affect your code without your awareness. Other languages might let you do other things, such as adding custom attributes, which you can use to identify tests. C++ defines standard attributes, which are intended to help the compiler optimize code execution or improve the compilation of your code. The standard C++ attributes are not something that we can use to identify tests and custom attributes would go against the tenet of unneeded features. I like this about C++, even if it means that we have to work a little harder to figure out which tests to run.
All we need to do is let each test identify itself. This is different from writing code that would try to find the tests. Finding the tests requires that they be marked in some way, such as using an attribute, so that they stand out and this isn’t possible in C++. Instead of finding them, we can use the constructor of each test functor so that they register themselves. The constructor for each test will add itself to the registry by pushing a pointer to itself onto a collection.
Once all the tests are registered through addition to a collection, we can go through the collection and run them all. We already simplified the tests so that they can all be run in the same way.
There’s just one complication that we need to be careful about. The test instances that are created in the TEST
macro are global variables and can be spread out over many different source files. Right now, we have a single test declared in a single main.cpp
source file. We’ll need to make sure that the collection that will eventually hold all the registered tests is set up and ready to hold the tests before we start trying to add tests to the collection. We’ll use a function to help coordinate the setup. This is the getTests
function, shown in the following code. The way getTests
works is not obvious and is described in more detail after the next code.
Now is also a good time to start thinking about a namespace to put the testing library into. We need a name for the namespace. I thought about what qualities stand out in this testing library. Especially when learning something like TDD, simplicity seems important, as is avoiding extra features that might not be needed. I came up with the word mere. I like the definition of mere: being nothing more nor better than. So, we’ll call the namespace MereTDD
.
Here is the first part of the Test.h
file with the new namespace and registration code added. We should also update the include
guard to something more specific, such as MERETDD_TEST_H
, like this:
#ifndef MERETDD_TEST_H
#define MERETDD_TEST_H
#include <string_view>
#include <vector>
namespace MereTDD
{
class TestInterface
{
public:
virtual ~TestInterface () = default;
virtual void run () = 0;
};
std::vector<TestInterface *> & getTests ()
{
static std::vector<TestInterface *> tests;
return tests;
}
} // namespace MereTDD
Inside the namespace, there is a new TestInterface
class declared with a run
method. I decided to move away from a functor and to this new design because when we need to actually run the test later, it looks more intuitive and understandable to have a method called run
.
The collection of tests is stored in a vector of TestInterface
pointers. This is a good place to use raw pointers because there is no ownership implied. The collection will not be responsible for deleting these pointers. The vector is declared as a static variable inside the getTests
function. This is to make sure that the vector is properly initialized, even if it is first accessed from another .cpp
source file compilation unit.
C++ language makes sure that global variables are initialized before main
begins. That means we have code in the test
instance constructors that get run before main
begins. When we have multiple .cpp
files later, making sure that the collection is initialized first becomes important. If the collection is a normal global variable that is accessed directly from another compilation unit, then it could be that the collection is not yet ready when the test tries to push itself onto the collection. Nevertheless, by going through the getTests
function, we avoid the readiness issue because the compiler will make sure to initialize the static vector the first time that the function is called.
We need to scope references to classes and functions declared inside the namespace anytime they are used within the macro. Here is the last part of Test.h
, with changes to the macro to use the namespace:
#define TEST \
class Test : public MereTDD::TestInterface \
{ \
public: \
Test (std::string_view name) \
: mName(name), mResult(true) \
{ \
MereTDD::getTests().push_back(this); \
} \
void run () override; \
private: \
std::string mName; \
bool mResult; \
}; \
Test test("testCanBeCreated"); \
void Test::run ()
#endif // MERETDD_TEST_H
The Test
constructor now registers itself by calling getTests
and pushing back a pointer to itself to the vector it gets. It doesn’t matter which .cpp
file is being compiled now. The collection of tests will be fully initialized once getTests
returns the vector.
The TEST
macro remains outside of the namespace because it doesn’t get compiled here. It only gets inserted into other code whenever the macro is used. That’s why inside the macro, it now needs to qualify TestInterface
and the getTests
call with the MereTDD
namespace.
Inside main.cpp
, the only change is how to call the test. We no longer refer to the test instance directly and now iterate through all the tests and call run
for each one. This is the reason I decided to use a method called run
instead of the function call operator:
int main ()
{
for (auto * test: MereTDD::getTests())
{
test->run();
}
return 0;
}
We can simplify this even more. The code in main
seems like it needs to know too much about how the tests are run. Let’s create a new function called runTests
to hold the for
loop. We might later need to enhance the for
loop and this seems like it should be internal to the test library. Here is what main
should look like now:
int main ()
{
MereTDD::runTests();
return 0;
}
We can enable this change by adding the runTests
function to Test.h
inside the namespace, like this:
namespace MereTDD
{
class TestInterface
{
public:
virtual ~TestInterface () = default;
virtual void run () = 0;
};
std::vector<TestInterface *> & getTests ()
{
static std::vector<TestInterface *> tests;
return tests;
}
void runTests ()
{
for (auto * test: getTests())
{
test->run();
}
}
} // namespace MereTDD
After all these changes, we have a simplified main
function that just calls on the test library to run all the tests. It doesn’t know anything about which tests are run or how. Even though we still have a single test, we’re creating a solid design that will support multiple tests.
The next section explains how you will use tests by looking at the first test.