6

Explore Improvements Early

We’ve come a long way with the testing library and have been using TDD the entire time to get us here. Sometimes, it’s important to explore new ideas before a project gets too far. After creating anything, we’ll have insights that we didn’t have at the beginning. And after working with a design for a while, we’ll develop a feel for what we like and what we might want to change. I encourage you to take this time to reflect on a design before proceeding.

We have something that is working and a bit of experience using it, so is there anything that we can improve?

This approach is like a higher-level process of TDD, as explained in Chapter 3, The TDD Process. First, we work out how we’d like to use something, then get it built, then do the minimal amount of work to get it working and the tests passing, and then enhance the design. We’ve got many things working now, but we haven’t gone so far yet where it would be too hard to change. We’re going to look at ways that the overall design could be enhanced.

At this point, it’s also a good idea to look around at other similar solutions and compare them. Get ideas. And try some new things to see whether they might be better. I’ve done this and would like to explore two topics in this chapter:

  • Can we use a new feature of C++ 20 to get line numbers instead of using __LINE__?
  • What would the tests look like if we used lambdas?

By the end of this chapter, you’ll understand the importance of and the process involved in exploring improvements early on in the design of your projects. Even if you don’t always decide to accept new ideas and make changes, your project will be better because you have taken the time to consider alternatives.

Technical requirements

The code in this chapter uses standard C++, and we will try out a feature introduced in C++ 20. The code is based on and continues from the previous chapters.

You can find all of the code for this chapter at the following GitHub repository:

https://github.com/PacktPublishing/Test-Driven-Development-with-CPP

Getting line numbers without macros

C++ 20 includes a new class that will help us get line numbers. In fact, it has a lot more information than just the line number. It includes the name of the file, the function name, and even the column number. However, we only need the line number. Note that at the time of writing this book, the implementation of this new class for my compiler has a bug. The end result is that I have had to put the code back to the way it was before the changes described in this section.

The new class is called source_location, and once it finally works correctly, we can change all of the existing confirm functions so that they accept std::source_location instead of the int for the line number. One example of an existing confirm function looks like this:

inline void confirm (
    bool expected,
    bool actual,
    int line)
{
    if (actual != expected)
    {
        throw BoolConfirmException(expected, line);
    }
}

We can eventually update the confirm functions to use std::source_location by changing all of the confirm functions, including the template override, to be similar to the following:

inline void confirm (
    bool expected, 
    bool actual,
    const std::source_location location = 
        std::source_location::current())
{
    if (actual != expected)
    {
        throw BoolConfirmException(expected, location.line());
    }
}

We’re not going to be making these changes right now because of the bug. The code does work as long as there is only a single source file in the project that tries to use source_location. The moment more than one source file tries to use source_location, there is a linker warning and the line method returns bad data. The bug should eventually get fixed, and I’m leaving this section in the book because it is a better approach. Depending on what compiler you are using, you might be able to start using source_location now.

Not only does the last parameter type and name change, but the usage needs to change when the line number is passed to the exception when it’s thrown. Notice how the new parameter includes a default value that gets set to the current location. The default parameter value means we no longer need to pass anything for the line number. The new location will get a default value that includes the current line number.

We need to include the header file for source_location at the top of Test.h, as follows:

#include <ostream>
#include <source_location>
#include <string_view>
#include <vector>

The macros that call confirm need to be updated to no longer worry about the line number:

#define CONFIRM_FALSE( actual ) 
    MereTDD::confirm(false, actual)
#define CONFIRM_TRUE( actual ) 
    MereTDD::confirm(true, actual)
#define CONFIRM( expected, actual ) 
    MereTDD::confirm(expected, actual)

Once source_location works properly, then we won’t really need these macros anymore. The first two are still useful because they eliminate the need to specify the expected bool value. Additionally, all three are slightly useful because they wrap up the specification of the MereTDD namespace. Even though we won’t technically need to keep using the macros, I like to keep using them because I think that the all-caps names help the confirmations stand out in the tests better.

This improvement would have been minor and limited to just the confirm functions and macros. So, should we still move to C++ 20 even though we can’t yet use source_location? I think so. If nothing else, this bug shows that changes are always being made to the standard libraries, and using the latest compiler and standard library is normally the best choice. Plus, there will be features we will use later in the book that are only found in C++20. For example, we’ll be using the std::map class and a useful method that was added in C++20 to determine whether the map contains an element already. We’ll be using concepts in Chapter 12, Creating Better Test Confirmations, which are only found in C++20.

The next improvement will be a bit more involved.

Exploring lambdas for tests

It’s getting more and more common for developers to avoid macros in their code. And I agree that there is almost no need for macros anymore. With std::source_location from the previous section, one of the last reasons to use macros has been eliminated.

Some companies might even have rules against using macros anywhere in their code. I think that’s a bit too much especially given the trouble with std::source_location. Macros still have the ability to wrap up code so that it can be inserted instead of the macro itself.

As the previous section shows, the CONFIRM_TRUE, CONFIRM_FALSE, and CONFIRM macros may no longer be absolutely necessary. I still like them. But if you don’t want to use them, then you don’t have to – at least once std::source_location works reliably in a large project.

The TEST and TEST_EX macros are still needed because they wrap up the declaration of the derived test classes, give them unique names, and set up the code so that the test body can follow. The result looks like we’re declaring a simple function. This is the effect we want. A test should be simple to write. What we have now is about as simple as it gets. But the design uses macros. Is there anything we can do to remove the need for the TEST and TEST_EX macros?

Whatever changes we make, we should keep the simplicity of declaring a test in Creation.cpp so that it looks similar to the following:

TEST("Test can be created")
{
}

What we really need is something that introduces a test, gives it a name, lets the test register itself, and then lets us write the body of the test function. The TEST macro provides this ability by hiding the declaration of a global instance of a class derived from the TestBase class. This declaration is left unfinished by the macro, so we can provide the body of the test function inside the curly braces. The other TEST_EX macro does something similar with the addition of catching the exception provided to the macro.

There is another way to write a function body in C++ without giving the function body a name. And that is to declare a lambda. What would a test look like if we stopped using the TEST macro and implemented the test function with a lambda instead? For now, let’s just focus on tests that do not expect an exception to be thrown. The following is what an empty test might look like:

Test test123("Test can be created") = [] ()
{
};

With this example, I’m trying to stick to the syntax needed by C++. This assumes we have a class called Test that we want to create an instance of. In this design, tests would reuse the Test class instead of defining a new class. The Test class would override the operator = method to accept a lambda. We need to give the instance a name so that the example uses test123. Why test123? Well, any object instance created still needs a unique name, so I’m using a number to provide something unique. We would need to continue using a macro to generate a unique number based on the line number if we decided to use this design. So, while this design avoids a new derived class for each test, it creates a new lambda for each test instead.

There’s a bigger problem with this idea. The code doesn’t compile. It might be possible to get the code to compile within a function. But as a declaration of a global Test instance, we can’t call an assignment operator. The best I can come up with would be to put the lambda inside the constructor as a new argument, as follows:

Test test123("Test can be created", [] ()
{
});

While it works for this test, it causes problems in the expected failure tests when we try to call the setExpectedFailureReason method because setExpectedFailureReason is not in scope within the lambda body. Also, we’re getting further away from the simple way we have now of declaring a test. The extra lambda syntax and the closing parenthesis and semicolon at the end make this harder to get right.

I’ve seen at least one other test library that does use lambdas and appears to avoid the need to declare a unique name and, thereby, avoid the need for a macro with something like this:

int main ()
{
    Test("Test can be created") = [] ()
    {
    };
    return 0;
};

But what this actually does is call a function named Test and pass the string literal as an argument. Then, the function returns a temporary object that overrides operator =, which is called to accept the lambda. The only place functions can be called is within other functions or class methods. That means a solution like this needs to declare tests from within a function, and the tests cannot be declared globally as instances like we are doing.

Usually, this means you declare all your tests from within the main function. Or you declare your tests as simple functions and call those functions from within main. Either way, you end up modifying main to call every test function. If you forget to modify main, then your test won’t get run. We’re going to keep main simple and uncluttered. The only thing main will do in our solution is run the tests that have been registered.

Even though lambdas won’t work for us because of the added complexity and because of the inability to call test methods such as setExpectedFailureReason, we can improve the current design a bit. The TEST and, especially, TEST_EX macros are doing work that we can remove from the macros.

Let’s start by modifying the TestBase class in Test.h so that it registers itself instead of doing the registration with derived classes in the macros. Also, we need to move the getTests function right before the TestBase class. And we need to forward declare the TestBase class since getTests uses a pointer to TestBase, like this:

class TestBase;
inline std::vector<TestBase *> & getTests ()
{
    static std::vector<TestBase *> tests;
    return tests;
}
class TestBase
{
public:
    TestBase (std::string_view name)
    : mName(name), mPassed(true), mConfirmLocation(-1)
    {
        getTests().push_back(this);
    }

We’ll keep the rest of TestBase unchanged because it handles properties such as the name and whether the test passed or not. We still have derived classes, but the goal of this simplification is to remove any work that the TEST and TEST_EX macros need to perform.

Most of the work that the TEST macro needs to do is to declare a derived class with a run method that will be filled in. The need to register the test is now handled by TestBase. The TEST_EX macro can be simplified further by creating another class called TestExBase, which will deal with the expected exception. Declare this new class right after TestBase. It looks like this:

template <typename ExceptionT>
class TestExBase : public TestBase
{
public:
    TestExBase (std::string_view name,
        std::string_view exceptionName)
    : TestBase(name), mExceptionName(exceptionName)
    { }
    void runEx () override
    {
        try
        {
            run();
        }
        catch (ExceptionT const &)
        {
            return;
        }
        throw MissingException(mExceptionName);
    }
private:
    std::string mExceptionName;
};

The TestExBase class derives from TestBase and is a template class designed to catch the expected exception. This code is currently written into TEST_EX, and we will change TEST_EX to use this new base class instead.

We’re ready to simplify the TEST and TEST_EX macros. The new TEST macro looks like this:

#define TEST( testName ) 
namespace { 
class MERETDD_CLASS : public MereTDD::TestBase 
{ 
public: 
    MERETDD_CLASS (std::string_view name) 
    : TestBase(name) 
    { } 
    void run () override; 
}; 
} /* end of unnamed namespace */ 
MERETDD_CLASS MERETDD_INSTANCE(testName); 
void MERETDD_CLASS::run ()

It’s slightly simpler than before. The constructor no longer needs to have code in the body because the registration is done in the base class.

The bigger simplification is in the TEST_EX macro, which looks like this:

#define TEST_EX( testName, exceptionType ) 
namespace { 
class MERETDD_CLASS : public MereTDD::TestExBase<exceptionType> 
{ 
public: 
    MERETDD_CLASS (std::string_view name, 
        std::string_view exceptionName) 
    : TestExBase(name, exceptionName) 
    { } 
    void run () override; 
}; 
} /* end of unnamed namespace */ 
MERETDD_CLASS MERETDD_INSTANCE(testName, #exceptionType); 
void MERETDD_CLASS::run ()

It’s a lot simpler than before because all the exception handling is done in its direct base class. Notice how the macro still needs to use the # operator for exceptionType when constructing the instance. Additionally, notice how it uses exceptionType without the # operator when specifying the template type to derive from.

Summary

This chapter explored ways in which to improve the test library by making use of a new feature in C++ 20 to get line numbers from the standard library instead of from the preprocessor. Even though the new code doesn’t work right now, it will eventually make the CONFIRM_TRUE, CONFIRM_FALSE, and CONFIRM macros optional. You will no longer have to use the macros. But I still like to use them because they help wrap up code that is easy to get wrong. And the macros are easier to spot in the tests because they use all capital letters.

We also explored a trend to avoid macros when declaring tests and what it would look like if we used lambdas instead. The approach almost worked with a more complicated test declaration. The extra complexity doesn’t matter though because the design did not work for all the tests.

It is still valuable for you to read about the proposed changes. You can learn about how other test libraries might work and understand why this book explains a solution that embraces macros.

This chapter has also shown you how to follow the TDD process at a higher level. The step in the process to enhance a test can be applied to an overall design. We were able to improve and simplify the TEST and TEST_EX macros, which makes all of the tests better.

The next chapter will explore what will be needed to add code that will run before and after the tests to help get things ready for the tests and clean things up after the tests finish.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.75.227