Search icon CANCEL
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

What information does a test need?

The current test function doesn’t really know its name. We want the test to have a name so that it can be identified but does that name really need to be the name of the function? It would be better if the name was available as data so it could be displayed without hardcoding the name inside the test body.

Equally, the current test function doesn’t have any idea of success or failure. We purposefully ignored the test result until now, but let’s think about it. Is it enough for a test function to return the status? Maybe it needs a bool return type where true would mean success and false would mean the test failed.

That might be a bit too simplistic. Sure, it would be enough for now, but if a test fails, it might be good to know why. A bool return type won’t be enough later. Instead of designing the entire solution, we just need to figure out what to do that will meet the expected needs.

Since we already know that we need some data to hold the test name, what if we now add simple bool result data in the same place? This would let us keep the test function return type as void, and it leaves room for a more advanced solution later.

Let’s change the test function into a functor as follows so that we can add member data for the name and result. This new design moves away from using a simple function for a test. We need a class to hold the data for the name and result. A functor is a class that can be called like a function using operator(), as this code shows:

#include <iostream>
#include <string_view>
class Test
{
public:
    Test (std::string_view name)
    : mName(name), mResult(true)
    {}
    void operator () ()
    {
        std::cout << mName << std::endl;
    }
private:
    std::string mName;
    bool mResult;
};
Test test("testCanBeCreated");
int main ()
{
    test();
    return 0;
}

The biggest problem with this is that we no longer have a simple way to write a test as if it was a simple function. By providing operator (), or function call operator, we created a functor that will let us call the class as if it was a function from within the main function. However, it’s more code to write. It solves the problem of the test name, gives us a simple solution for the result, which can be expanded later, and also solves another problem that wasn’t obvious before.

When we called the test function in main before, we had to call it by the function name. That’s how functions are called in code, right? This new design eliminates that coupling by creating an instance of the Test functor called test. Now, main doesn’t care about the test name. It only refers to the instance of the functor. The only place in which the name of the test now appears in the code is when the functor instance is created.

We can fix the problem of all the extra code needed to write a test by using a macro. Macros are not needed in C++ as they used to be and some people might even think that they should be removed from the language entirely. They do have a couple of good uses left and wrapping up code into a macro is one of them.

We’ll eventually put the macro definition into a separate header file, which will become the test library. What we want to do is wrap up all the functor code in the macro but leave the implementation of the actual test function body to be written as if everything was a normal function.

First, we’ll make a simple change to move the implementation of the test function body outside of the class definition, like this. The function call operator is the method that needs to be moved outside:

class Test
{
public:
    Test (std::string_view name)
    : mName(name), mResult(true)
    {}
    void operator () ();
private:
    std::string mName;
    bool mResult;
};
Test test("testCanBeCreated");
void Test::operator () ()
{
    std::cout << mName << std::endl;
}

Then, the class definition, instance declaration, and first line of the function call operator can be turned into a macro. Compare the following code with the previous code to see how the Test class is turned into the TEST macro. By itself, this macro would not compile because it leaves the function call operator in an unfinished state. That’s exactly what we want because it lets the code use the macro like a function signature declaration and finish it up by providing the code inside the curly braces:

#define TEST class Test \
{ \
public: \
    Test (std::string_view name) \
    : mName(name), mResult(true) \
    {} \
    void operator () (); \
private: \
    std::string mName; \
    bool mResult; \
}; \
Test test("testCanBeCreated"); \
void Test::operator () ()
TEST
{
    std::cout << mName << std::endl;
}

Because the macro is defined over multiple lines, each line except the last needs to end with a backslash. The macro is a little more compact because the empty lines have been removed. This is a personal choice and you can leave the empty lines if you want. An empty line still needs the backslash though, which defeats the purpose of having an empty line.

The code uses the TEST macro with the unfinished function call operator just like a function definition, but then it completes the code by providing the curly braces and method implementation needed.

We’re making progress! It might be hard to see it because everything is in a single file. Let’s fix that by creating a new file called Test.h and moving the macro definition to the new file, like this:

#ifndef TEST_H
#define TEST_H
#include <string_view>
#define TEST class Test \
{ \
public: \
    Test (std::string_view name) \
    : mName(name), mResult(true) \
    {} \
    void operator () (); \
private: \
    std::string mName; \
    bool mResult; \
}; \
Test test("testCanBeCreated"); \
void Test::operator () ()
#endif // TEST_H

Now, we can go back to simpler code in main.cpp, like this next block of code shows. All we need to do is include Test.h and we can use the macro:

#include "Test.h"
#include <iostream>
TEST
{
    std::cout << mName << std::endl;
}
int main ()
{
    test();
    return 0;
}

We now have something that’s beginning to look like the simple function we started with, but there’s a lot of code hidden inside the TEST macro to make it seem simple.

In the next section, we’ll fix the need for main to call test() directly. The name of the functor, test, is a detail that should not be known outside of the macro, and we definitely shouldn’t need to call a test directly to run it, no matter what it’s called.

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