Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Test-Driven Development with C++

You're reading from   Test-Driven Development with C++ A simple guide to writing bug-free Agile code

Arrow left icon
Product type Paperback
Published in Nov 2022
Publisher Packt
ISBN-13 9781803242002
Length 430 pages
Edition 1st Edition
Languages
Concepts
Arrow right icon
Author (1):
Arrow left icon
Abdul Wahid Tanner Abdul Wahid Tanner
Author Profile Icon Abdul Wahid Tanner
Abdul Wahid Tanner
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Part 1: Testing MVP
2. Chapter 1: Desired Test Declaration FREE CHAPTER 3. Chapter 2: Test Results 4. Chapter 3: The TDD Process 5. Chapter 4: Adding Tests to a Project 6. Chapter 5: Adding More Confirm Types 7. Chapter 6: Explore Improvements Early 8. Chapter 7: Test Setup and Teardown 9. Chapter 8: What Makes a Good Test? 10. Part 2: Using TDD to Create a Logging Library
11. Chapter 9: Using Tests 12. Chapter 10: The TDD Process in Depth 13. Chapter 11: Managing Dependencies 14. Part 3: Extending the TDD Library to Support the Growing Needs of the Logging Library
15. Chapter 12: Creating Better Test Confirmations 16. Chapter 13: How to Test Floating-Point and Custom Values 17. Chapter 14: How to Test Services 18. Chapter 15: How to Test With Multiple Threads 19. Index 20. Other Books You May Enjoy

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.

You have been reading a chapter from
Test-Driven Development with C++
Published in: Nov 2022
Publisher: Packt
ISBN-13: 9781803242002
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime