12

Creating Better Test Confirmations

This chapter introduces Part 3, where we extend the TDD library to support the growing needs of the logging library. Part 1, Testing MVP, of this book developed a basic unit test library, and Part 2, Logging Library, started to use the unit test library to build a logging library. Now we are following TDD, which encourages enhancing something once the basic tests are working.

Well, we managed to get a basic unit test library working and proved its worth by building a logging library. In a way, the logging library is like systems tests for the unit test library. Now it’s time to enhance the unit test library.

This chapter adds a completely new type of confirmation to the unit test library. First, we’ll look at the existing confirmations to understand how they can be improved and what the new solution will look like.

The new confirmations will be more intuitive, more flexible, and extensible. And remember to pay attention not only to the code being developed in this chapter but also to the process. That’s because we’ll be using TDD throughout to write some tests, starting with a simple solution and then enhancing the tests to create an even better solution.

In this chapter, we will cover the following main topics:

  • The problem with the current confirmations
  • How to simplify string confirmations
  • Enhancing the unit test library to support Hamcrest-style confirmations
  • Adding more Hamcrest matcher types

Technical requirements

All code in this chapter uses standard C++ that builds on any modern C++ 20 or later compiler and standard library. The code is based on and continues enhancing the testing library from Part 1, Testing MVP, of this book.

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

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

The problem with the current confirmations

Before we begin making changes, we should have some idea of why. TDD is all about the customer experience. How can we design something that is easy and intuitive to use? Let’s start by taking a look at a couple of existing tests:

TEST("Test string and string literal confirms")
{
    std::string result = "abc";
    CONFIRM("abc", result);
}
TEST("Test float confirms")
{
    float f1 = 0.1f;
    float f2 = 0.2f;
    float sum = f1 + f2;
    float expected = 0.3f;
    CONFIRM(expected, sum);
}

These tests have served well and are easy, right? What we’re looking at here is not the tests themselves but the confirmations. This style of confirmation is called the classic style.

How would we speak or read aloud the first confirmation? It might go like this: “Confirm the expected value of abc matches the value of result.”

That’s not too bad, but it’s a bit awkward. That’s not how a person would normally talk. Without looking at any code, a more natural way to say the same thing would be: “Confirm that result equals abc.”

At first glance, maybe all we need to do is reverse the order of the parameters and put the actual value first followed by the expected value. But there’s a piece missing. How do we know that a confirmation is checking for equality? We know because that’s the only thing the existing confirm functions know how to check. Also, that means the CONFIRM macro only knows how to check for equality, too.

We have a better solution for bool values because we created special CONFIRM_TRUE and CONFIRM_FALSE macros that are easy to use and understand. And because the bool versions only take a single parameter, there’s no question of expected versus actual ordering.

There’s a better solution that aligns with the more natural way we would speak about a confirmation. The better solution uses something called matchers and is referred to as the Hamcrest style. The name “Hamcrest” is just a reordering of the letters in the word “matchers.” Here is what a test would look like written in the Hamcrest style:

TEST("Test can use hamcrest style confirm")
{
    int ten = 10;
    CONFIRM_THAT(ten, Equals(10));
}

We’re not really designing the Hamcrest style in this book. The style already exists and is common in other testing libraries. And the main reason the testing library in this book puts the expected value first followed by the actual value for the classical style is to follow the common practice of the classical style.

Imagine you were to reinvent a better light switch. And I’ve been in buildings that have tried. The light switch might actually be better in some way. But if it doesn’t follow normal expectations, then people will get confused.

The same is true of the classical confirmations we started with in this book. I could have designed the confirmations to put the actual value first and maybe that would be better. But it would be unexpected for anybody who is even a little familiar with the existing testing libraries.

This brings up a great point to consider when creating designs using TDD. Sometimes, an inferior solution is better when that’s what the customer expects. Remember that whatever we design should be easy and intuitive to use. The goal is not to make the ultimate and most modern design but to make something that the user will be happy with.

This is why Hamcrest matchers work. The design doesn’t just switch the order of the expected and actual values because switching the order by itself would only confuse users.

Hamcrest works well because something else was added: the matcher. Notice the Equals(10) part of the confirmation. Equals is a matcher that makes it clear what the confirmation is doing. The matcher, together with a more intuitive ordering, gives a solution enough benefits to overcome the natural reluctance people have with switching to a new way of doing things. The Hamcrest style is not just a better light switch. Hamcrest is different enough and provides enough value that it avoids the confusion of a slightly better but different solution.

Also, notice that the name of the macro has changed from CONFIRM to CONFIRM_THAT. The name change is another way to avoid confusion and lets users continue to use the older classical style or opt for the newer Hamcrest style.

Now that we have a place to specify something such as Equals, we can also use different matchers such as GreaterThan or BeginsWith. Imagine that you wanted to confirm that some text begins with some expected characters. How would you write a test like that using classical confirmations? You would have to check for the beginning text outside of the confirmation and then confirm the result of the check. With the Hamcrest style and an appropriate matcher, you can confirm the text with a single-line confirmation. And you get the benefit of a more readable confirmation that makes it clear what is being confirmed.

What if you can’t find a matcher that fits your needs? You can always write your own to do exactly what you need. So, Hamcrest is extensible.

Before diving into the new Hamcrest design, the next section will take a slight detour to explain an improvement to the existing classic confirm template function. This improvement will be used in the Hamcrest design, so understanding the improvement first will help later when we get to the Hamcrest code explanations.

Simplifying string confirmations

While I was writing the code for this chapter, I ran into a problem confirming string data types that reminded me of how we added support for confirming strings in Chapter 5, Adding More Confirm Types. The motivating factor from Chapter 5 was to get the code to compile because we can’t pass std::string to a std::to_string function. I’ll briefly explain the problem again here.

I’m not sure of the exact reasons, but I think that the C++ standard library designers felt there was no need to provide an overload of std::to_string that accepts std::string because no conversion is needed. A string is already a string! Why convert something into what it already is?

Maybe this decision was on purpose or maybe it was an oversight. But it sure would have helped to have a string conversion into a string for template functions that need to convert their generic types into strings. That’s because, without the overload, we have to take extra steps to avoid compile errors. What we need is a to_string function that can convert any type into a string even if the type is already a string. If we had this ability to always be able to convert types into strings, then a template wouldn’t need to be specialized for strings.

In Chapter 5, we introduced this template:

template <typename T>
void confirm (
    T const & expected,
    T const & actual,
    int line)
{
    if (actual != expected)
    {
        throw ActualConfirmException(
            std::to_string(expected),
            std::to_string(actual),
            line);
    }
}

The confirm function accepts two templatized parameters, called expected and actual, that are compared for equality. If they are not equal, then the function passes both parameters to an exception that gets thrown. The parameters need to be converted into strings, as needed, by the ActualConfirmException constructor.

This is where we run into a problem. If the confirm template function is called with strings, then it doesn’t compile because strings can’t be converted into strings by calling std::to_string.

The solution we went with in Chapter 5 was to overload the confirm function with a non-template version that directly accepted strings. We actually created two overloads, one for strings and one for string views. This solved the problem but left us with the following two additional overloads:

inline void confirm (
    std::string_view expected,
    std::string_view actual,
    int line)
{
    if (actual != expected)
    {
        throw ActualConfirmException(
            expected,
            actual,
            line);
    }
}
inline void confirm (
    std::string const & expected,
    std::string const & actual,
    int line)
{
    confirm(
        std::string_view(expected),
        std::string_view(actual),
        line);
}

When calling confirm with strings, these overloads are used instead of the template. The version that accepts std::string types calls into the version that takes std::string_view types, which uses the expected and actual parameters directly without trying to call std::to_string.

At the time, this wasn’t such a bad solution because we already had extra overloads of confirm for bools and the various floating point types. Two more overloads for strings was okay. Later, you’ll see how a small change will let us remove these two string overloads.

And now we come back to the problem of converting string data types with the new Hamcrest design we’ll be working on in this chapter. We will no longer need extra overloads of confirm even for bool or floating point types. As I was working on a new solution, I came back to the earlier solution from Chapter 5 and decided to refactor the existing classic confirms so that both solutions would be similar.

We’ll get into the new design later in this chapter. But so that we don’t have to interrupt that explanation, I have decided to take the detour now and explain how to remove the need for the string and string view overloads of the classic confirm function. Going through the explanation now should also make it easier to understand the new Hamcrest design since you’ll already be familiar with this part of the solution.

Also, I’d like to add that TDD helps with this type of refactoring. Because we already have existing tests for the classic confirms, we can remove the string overloads of confirm and make sure that all the tests continue to pass. I’ve worked on projects before where only the new code would use the better solution and we would have to leave the existing code unchanged in order to avoid introducing bugs. Doing this just makes the code harder to maintain because now there would be two different solutions in the same project. Having good tests helps give you the confidence needed to change existing code.

Okay, the core of the problem is that the C++ standard library does not include overloads of to_string that work with strings. And while it might be tempting to just add our own version of to_string to the std namespace, this is not allowed. It would probably work, and I’m sure that lots of people have done this. But it’s technically undefined behavior to add any function into the std namespace. There are very specific cases where we are allowed to add something into the std namespace, and unfortunately, this is not one of the allowed exceptions to the rules.

We will need our own version of to_string. We just can’t put our version in the std namespace. That’s a problem because when we call to_string, we currently specify the namespace by calling std::to_string. What we need to do is simply call to_string without any namespace and let the compiler look in either the std namespace to find the versions of to_string that work with numeric types, or look in our namespace to find our new version that works with strings. The new to_string function and the modified confirm template function look like this:

inline std::string to_string (std::string const & str)
{
    return str;
}
template <typename ExpectedT, typename ActualT>
void confirm (
    ExpectedT const & expected,
    ActualT const & actual,
    int line)
{
    using std::to_string;
    using MereTDD::to_string;
    if (actual != expected)
    {
        throw ActualConfirmException(
            to_string(expected),
            to_string(actual),
            line);
    }
}

We can remove the two overloads of confirm that take string views and strings. Now, the confirm template function will work for strings.

With the new to_string function that accepts std::string, all it needs to do is return the same string. We don’t really need another to_string function that works with string views.

The confirm template function is a little more complicated because it now needs two types, ExpectedT and ActualT. The two types are needed for those cases when we need to compare a string literal with a string, such as in the following test:

TEST("Test string and string literal confirms")
{
    std::string result = "abc";
    CONFIRM("abc", result);
}

The reason this test used to compile when we had only a single confirm template parameter is that it wasn’t calling into the template. The compiler was converting the "abc" string literal into a string and calling the overload of confirm that accepted two strings. Or maybe it was converting both the string literal and the string into string views and calling the overload of confirm that accepted two string views. Either way, because we had separate overloads of confirm, the compiler was able to make it work.

Now that we removed both confirm overloads that deal with strings, we have only the template, and we need to let it accept different types in order to compile. I know, we still have overloads for bool and the floating point types. I’m only talking about the string overloads that we can remove.

In the new template, you can see that we call to_string without any namespace specification. The compiler is able to find the versions of to_string it needs because of the two using statements inside the template function. The first using statement tells the compiler that it should consider all the to_string overloads in the std namespace. And the second using statement tells the compiler to also consider any to_string functions it finds in the MereTDD namespace.

The compiler is now able to find a version of to_string that works with the numeric types when confirm is called with numeric types. And the compiler can find our new to_string function that works with strings when needed. We no longer need to limit the compiler to only look in the std namespace.

Now we can go back to the new Hamcrest style design, which we will do in the next section. The Hamcrest design will, eventually, use a solution similar to what was just described here.

Enhancing the test library to support Hamcrest matchers

Once you get a basic implementation working and passing the tests, TDD guides us to enhance the design by creating more tests and then getting the new tests to pass. That’s exactly what this chapter is all about. We’re enhancing the classic style confirmations to support the Hamcrest style.

Let’s start by creating a new file, called Hamcrest.cpp, in the tests folder. Now, the overall project structure should look like this:

MereTDD project root folder
    Test.h
    tests folder
        main.cpp
        Confirm.cpp
        Creation.cpp
        Hamcrest.cpp
        Setup.cpp

If you’ve been following all the code in this book so far, remember that we’re going back to the MereTDD project that we last worked on in Chapter 7, Test Setup and Teardown. This is not the MereMemo logging project.

The Hamcrest style test that we need to support goes inside Hamcrest.cpp so that the new file looks like this:

#include "../Test.h"
TEST("Test can use hamcrest style confirm")
{
    int ten = 10;
    CONFIRM_THAT(ten, Equals(10));
}

We might as well start with the new CONFIRM_THAT macro, which goes at the end of Test.h right after the other CONFIRM macros, like this:

#define CONFIRM_FALSE( actual ) 
    MereTDD::confirm(false, actual, __LINE__)
#define CONFIRM_TRUE( actual ) 
    MereTDD::confirm(true, actual, __LINE__)
#define CONFIRM( expected, actual ) 
    MereTDD::confirm(expected, actual, __LINE__)
#define CONFIRM_THAT( actual, matcher ) 
    MereTDD::confirm_that(actual, matcher, __LINE__)

The CONFIRM_THAT macro is similar to the CONFIRM macro except that the actual parameter comes first, and instead of an expected parameter, we have a parameter called matcher. We’ll also call a new function called confirm_that. The new function helps make it simpler to keep the classic style confirm overloads separate from the Hamcrest-style confirm_that function.

We don’t need all the overloads in the same way that we needed for confirm. The confirm_that function can be implemented with a single template function. Place this new template in Test.h right after the classic confirm template function. Both template functions should look like this:

template <typename ExpectedT, typename ActualT>
void confirm (
    ExpectedT const & expected,
    ActualT const & actual,
    int line)
{
    using std::to_string;
    using MereTDD::to_string;
    if (actual != expected)
    {
        throw ActualConfirmException(
            to_string(expected),
            to_string(actual),
            line);
    }
}
template <typename ActualT, typename MatcherT>
inline void confirm_that (
    ActualT const & actual,
    MatcherT const & matcher,
    int line)
{
    using std::to_string;
    using MereTDD::to_string;
    if (not matcher.pass(actual))
    {
        throw ActualConfirmException(
            to_string(matcher),
            to_string(actual),
            line);
    }
}

We’re only adding the confirm_that function. I decided to show both functions so that you can see the differences easier. Notice that now, the ActualT type is given first. The order doesn’t really matter, but I like to put the template parameters in a reasonable order. We no longer have an ExpectedT type; instead, we have a MatcherT type.

The name of the new template function is different too, so there is no ambiguity due to the similar template parameters. The new template function is called confirm_that.

While the classic confirm function compares the actual parameter directly with the expected parameter, the new confirm_that function calls into a pass method on the matcher to perform the check. We don’t really know what the matcher will be doing in the pass method because that is for the matcher to decide. And because any changes in the comparison from one type to another are wrapped up in the matcher, we don’t need to overload the confirm_that function like we had to do for the classic confirm function. We’ll still need a special code, but the differences will be handled by the matcher in this design.

This is where I realized that there needs to be a different solution for converting the matcher and actual parameters into strings. It seems pointless to override confirm_that just to avoid calling to_string when the type of ActualT is a string. So, I stopped calling std::to_string(actual) and instead started calling to_string(actual). In order for the compiler to find the necessary to_string functions, the using statements are needed. This is the explanation that the previous section describes for simplifying the string comparisons.

Now that we have the confirm_that template, we can focus on the matcher. We need to be able to call a pass method and convert a matcher into a string. Let’s create a base class for all the matchers to inherit from, so they will all have a common interface. Place this base class and to_string function right after the confirm_that function in Test.h, as follows:

class Matcher
{
public:
    virtual ~Matcher () = default;
    Matcher (Matcher const & other) = delete;
    Matcher (Matcher && other) = delete;
    virtual std::string to_string () const = 0;
    Matcher & operator = (Matcher const & rhs) = delete;
    Matcher & operator = (Matcher && rhs) = delete;
protected:
    Matcher () = default;
};
inline std::string to_string (Matcher const & matcher)
{
    return matcher.to_string();
}

The to_string function will let us convert a matcher into a string by calling the virtual to_string method in the Matcher base class. Notice there is no pass method in the Matcher class.

The Matcher class itself is a base class that doesn’t need to be copied or assigned. The only common interface the Matcher class defines is a to_string method that all matchers will implement to convert themselves into a string that can be sent to the test run summary report.

What happened to the pass method? Well, the pass method needs to accept the actual type that will be used to determine whether the actual value matches the expected value. The expected value itself will be held in the derived matcher class. The actual value will be passed to the pass method.

The types of values accepted for the actual and expected values will be fully under the control of the derived matcher class. Because the types can change from one usage of a matcher to another, we can’t define a pass method in the Matcher base class. This is okay because the confirm_that template doesn’t work with the Matcher base class. The confirm_that template will have knowledge of the real matcher-derived class and can call the pass method directly as a non-virtual method.

The to_string method is different because we want to call the virtual Matcher::to_string method from within the to_string helper function that accepts any Matcher reference.

So, when converting a matcher into a string, we treat all matchers the same and go through the virtual to_string method. And when calling pass, we work directly with the real matcher class and call pass directly.

Let’s see what a real matcher class will look like. The test we are implementing uses a matcher called Equals. We can create the derived Equals class right after the Matcher class and the to_string function, as follows:

template <typename T>
class Equals : public Matcher
{
public:
    Equals (T const & expected)
    : mExpected(expected)
    { }
    bool pass (T const & actual) const
    {
        return actual == mExpected;
    }
    std::string to_string () const override
    {
        using std::to_string;
        using MereTDD::to_string;
        return to_string(mExpected);
    }
private:
    T mExpected;
};

The Equals class is another template because it needs to hold the proper expected value type, and it needs to use the same type in the pass method for the actual parameter.

Notice that the to_string override method uses the same solution to convert the mExpected data member into a string that we’ve been using. We call to_string and let the compiler find an appropriate match in either the std or MereTDD namespaces.

We need one more small change to get everything working. In our Hamcrest test, we use the Equals matcher without any namespace specification. We could refer to it as MereTDD::Equals. But the namespace specification distracts from the readability of the tests. Let’s add a using namespace MereTDD statement to the top of any test file that will use Hamcrest matchers so we can refer to them directly, like this:

#include "../Test.h"
using namespace MereTDD;
TEST("Test can use hamcrest style confirm")
{
    int ten = 10;
    CONFIRM_THAT(ten, Equals(10));
}

That’s everything needed to support our first Hamcrest matcher unit test – building and running show that all tests pass. What about an expected failure? First, let’s create a new test like this:

TEST("Test hamcrest style confirm failure")
{
    int ten = 10;
    CONFIRM_THAT(ten, Equals(9));
}

This test is designed to fail because 10 will not equal 9. We need to build and run once just to get the failure message from the summary report. Then, we can add a call to setExpectedFailureReason with the exactly formatted failure message. Remember that the failure message needs to match exactly, including all the spaces and punctuation. I know this can be tedious, but it should not be a test that you need to worry about unless you’re testing one of your own custom matchers to make sure the custom matcher is able to format a proper error message.

After getting the exact error message, we can modify the test to turn it into an expected failure, as follows:

TEST("Test hamcrest style confirm failure")
{
    std::string reason = "    Expected: 9
";
    reason += "    Actual  : 10";
    setExpectedFailureReason(reason);
    int ten = 10;
    CONFIRM_THAT(ten, Equals(9));
}

Building and running again shows both Hamcrest tests results, as follows:

------- Test: Test can use hamcrest style confirm
Passed
------- Test: Test hamcrest style confirm failure
Expected failure
    Expected: 9
    Actual  : 10

This is a good start. We haven’t yet started talking about how to design custom matchers. Before we start custom matchers, what about other basic types? We only have a couple of Hamcrest tests that compare int values. The next section will explore other basic types and add more tests.

Adding more Hamcrest types

There is a pattern to using TDD that you should be familiar with by now. We add a little bit of something, get it working, and then add more. We have the ability to confirm int values with a Hamcrest Equals matcher. Now it’s time to add more types. Some of these types might work without any extra work due to the template confirm_that function. Other types might need changes. We’ll find out what needs to be done by writing some tests.

The first test ensures that the other integer types work as expected. Add this test to Hamcrest.cpp:

TEST("Test other hamcrest style integer confirms")
{
    char c1 = 'A';
    char c2 = 'A';
    CONFIRM_THAT(c1, Equals(c2));
    CONFIRM_THAT(c1, Equals('A'));
    short s1 = 10;
    short s2 = 10;
    CONFIRM_THAT(s1, Equals(s2));
    CONFIRM_THAT(s1, Equals(10));
    unsigned int ui1 = 3'000'000'000;
    unsigned int ui2 = 3'000'000'000;
    CONFIRM_THAT(ui1, Equals(ui2));
    CONFIRM_THAT(ui1, Equals(3'000'000'000));
    long long ll1 = 5'000'000'000'000LL;
    long long ll2 = 5'000'000'000'000LL;
    CONFIRM_THAT(ll1, Equals(ll2));
    CONFIRM_THAT(ll1, Equals(5'000'000'000'000LL));
}

First, the test declares a couple of chars and uses the Equals matcher in a couple of different ways. The first is to test for equality with another char. The second uses a char literal value, 'A', for the comparison.

The second set of confirmations is based on short ints. We use the Equals matcher with another short int and then an int literal value of 10 for the comparison.

The third set of confirmations is based on unsigned ints and, again, tries to use the Equals matcher with another variable of the same type and with a literal int.

The fourth set of confirmations makes sure that long long types are supported.

We’re not creating helper functions designed to simulate other software being tested. You already know how to use confirmations in a real project based on the tests in the logging library. That’s why this test makes things simple and just focuses on making sure that the CONFIRM_THAT macro, which calls the confirm_that template function, works.

Building and running these tests show that all tests pass with no changes or enhancements needed.

What about bool types? Here is a test that goes into Hamcrest.cpp to test bool types:

TEST("Test hamcrest style bool confirms")
{
    bool b1 = true;
    bool b2 = true;
    CONFIRM_THAT(b1, Equals(b2));
    // This works but probably won't be used much.
    CONFIRM_THAT(b1, Equals(true));
    // When checking a bool variable for a known value,
    // the classic style is probably better.
    CONFIRM_TRUE(b1);
}

This test shows that the Hamcrest style works for bool types, too. When comparing one bool variable with another, the Hamcrest style is better than the classic style. However, when comparing a bool variable with an expected true or false literal, it’s actually more readable to use the classic style because we have simplified CONFIRM_TRUE and CONFIRM_FALSE macros.

Now, let’s move on to strings with this test that goes into Hamcrest.cpp. Note that this test will fail to compile at first and that’s okay. The test looks like this:

TEST("Test hamcrest style string confirms")
{
    std::string s1 = "abc";
    std::string s2 = "abc";
    CONFIRM_THAT(s1, Equals(s2));     // string vs. string
    CONFIRM_THAT(s1, Equals("abc"));  // string vs. literal
    CONFIRM_THAT("abc", Equals(s1));  // literal vs. string
}

There are several confirms in this test, and that’s okay because they’re all related. The comments help to clarify what each confirmation is testing.

We’re always looking for two things with a new test. The first is whether the test compiles at all. And the second is whether it passes. Right now, the test will fail to compile with an error similar to the following:

MereTDD/tests/../Test.h: In instantiation of 'MereTDD::Equals<T>::Equals(const T&) [with T = char [4]]':
MereTDD/tests/Hamcrest.cpp:63:5:   required from here
MereTDD/tests/../Test.h:209:7: error: array used as initializer
  209 |     : mExpected(expected)
      |       ^~~~~~~~~~~~~~~~~~~

You might get different line numbers, so I’ll explain what the error is referring to. The failure is in the Equals constructor, which looks like this:

    Equals (T const & expected)
    : mExpected(expected)
    { }

And line 63 in Hamcrest.cpp is the following line:

    CONFIRM_THAT(s1, Equals("abc"));  // string vs. literal

We’re trying to construct an Equals matcher given the "abc" string literal, and this fails to compile. The reason is that the T type is an array that needs to be initialized in a different manner.

What we need is a special version of Equals that works with string literals. Since a string literal is an array of constant chars, the following template specialization will work. Place this new template in Test.h right after the existing Equals template:

template <typename T, std::size_t N> requires (
    std::is_same<char, std::remove_const_t<T>>::value)
class Equals<T[N]> : public Matcher
{
public:
    Equals (char const (& expected)[N])
    {
        memcpy(mExpected, expected, N);
    }
    bool pass (std::string const & actual) const
    {
        return actual == mExpected;
    }
    std::string to_string () const override
    {
        return std::string(mExpected);
    }
private:
    char mExpected[N];
};

We’ll need a couple of extra includes in Test.h for cstring and type_traits, as follows:

#include <cstring>
#include <map>
#include <ostream>
#include <string_view>
#include <type_traits>
#include <vector>

The template specialization uses a new C++20 feature, called requires, which helps us to place constraints on template parameters. The requires keyword is actually part of a bigger enhancement in C++20, called concepts. Concepts are a huge enhancement to C++ and a full explanation would be beyond the scope of this book. We’re using concepts and the requires keyword to simplify the template specialization to only work with strings. The template itself takes a T type like before and a new numeric value, N, which will be the size of the string literal. The requires clause makes sure that T is a char. We need to remove the const qualifier from T because string literals are actually constant.

The Equals specialization then says it is an array of T[N]. The constructor takes a reference to an array of N chars, and instead of trying to directly initialize mExpected with the constructor’s expected parameter, it now calls memcpy to copy the chars from the literal into the mExpected array. The strange syntax of char const (& expected)[N] is how C++ specifies an array as a method parameter that does not get decayed into a simple pointer.

Now the pass method can take a string reference as its actual parameter type since we know that we are dealing with strings. Additionally, the to_string method can directly construct and return std::string from the mExpected char array.

One interesting, and maybe only theoretical, benefit of the Equals template specialization and the pass method is that we can now confirm that a string literal equals another string literal. I can’t think of any place where this would be useful but it works, so we might as well add it to the test like this:

TEST("Test hamcrest style string confirms")
{
    std::string s1 = "abc";
    std::string s2 = "abc";
    CONFIRM_THAT(s1, Equals(s2));       // string vs. string
    CONFIRM_THAT(s1, Equals("abc"));    // string vs. literal
    CONFIRM_THAT("abc", Equals(s1));    // literal vs. string
    // Probably not needed, but this works too.
    CONFIRM_THAT("abc", Equals("abc")); // literal vs. Literal
}

What about char pointers? They’re not as common as char arrays for template parameters because char arrays come from working with string literals. A char pointer is slightly different. We should consider char pointers because while they are not as common in template parameters, a char pointer is probably more common overall than char arrays. Here is a test that demonstrates char pointers. Note that this test will not compile yet. Add this into Hamcrest.cpp:

TEST("Test hamcrest style string pointer confirms")
{
    char const * sp1 = "abc";
    std::string s1 = "abc";
    char const * sp2 = s1.c_str();    // avoid sp1 and sp2 being same
    CONFIRM_THAT(sp1, Equals(sp2));   // pointer vs. pointer
    CONFIRM_THAT(sp2, Equals("abc")); // pointer vs. literal
    CONFIRM_THAT("abc", Equals(sp2)); // literal vs. pointer
    CONFIRM_THAT(sp1, Equals(s1));    // pointer vs. string
    CONFIRM_THAT(s1, Equals(sp1));    // string vs. pointer
}

We can initialize a char pointer given a string literal just like how std::string is initialized. But while std::string copies the text into its own memory to manage, a char pointer just points to the first char in the string literal. I keep saying that we’re working with char pointers. But to be more specific, we’re working with constant char pointers. The code needs to use const, but I sometimes leave const out when speaking or writing.

The new test for the string pointer confirms the need to take extra steps to make sure that sp1 and sp2 point to different memory addresses.

String literals in C++ are consolidated so that duplicate literal values all point to the same memory address. Even though a literal such as "abc" might be used many times in the source code, there will only be one copy of the string literal in the final executable that gets built. The test must go through extra steps to make sure that sp1 and sp2 have different pointer values while maintaining the same text. Whenever std::string is initialized with a string literal, the text of the string literal gets copied into std::string to manage. The std::string might use dynamically allocated memory or local memory on the stack. A std::string will not just point to the memory address used in the initialization. If we simply initialized sp2 the same way as sp1, then both pointers would point to the same memory address. But by initializing sp2 to point to the string inside s1, then sp2 points to a different memory address from sp1. Even though sp1 and sp2 point to different memory addresses, the value of the text chars at each address is the same.

Okay, now that you understand what the test is doing, does it compile? No. The build fails while trying to call the pass method in the confirm_that template function.

The line in the test that causes the build failure is the last confirmation. The compiler is trying to convert the s1 string into a constant char pointer. But this is misleading because even if we comment out the last confirmation so that the build succeeds, the test then fails at runtime, like this:

------- Test: Test hamcrest style string pointer confirms
Failed confirm on line 75
    Expected: abc
    Actual  : abc

Because you might get different line numbers, I’ll explain that line 75 is the first confirmation from the test:

    CONFIRM_THAT(sp1, Equals(sp2));   // pointer vs. pointer

Look at the test failure message though. It says that "abc" is not equal to "abc"! What is going on?

Because we’re using the original Equals template class, it only knows that we are dealing with char pointers. When we call pass, it’s the pointer values that are being compared. And because we took extra steps to make sure that sp1 and sp2 have different pointer values, the test fails. And the test fails even though the text both pointers refer to is the same.

In order to support pointers, we’ll need another template specialization of Equals. But we can’t just specialize on any pointer type, in the same way we couldn’t specialize on any array type. We made sure that the array specialization only works for char arrays. So, we should also make sure that our pointer specialization only works with char pointers. Add this specialization right after the second Equals class in Test.h:

template <typename T> requires (
    std::is_same<char, std::remove_const_t<T>>::value)
class Equals<T *> : public Matcher
{
public:
    Equals (char const * expected)
    : mExpected(expected)
    { }
    bool pass (std::string const & actual) const
    {
        return actual == mExpected;
    }
    std::string to_string () const override
    {
        return mExpected;
    }
private:
    std::string mExpected;
};

With this third version of the Equals class, we not only fix the build error but all the confirmations pass too! This template specializes Equals for T * and also requires that T be a char type.

The constructor accepts a pointer to constant chars and initializes mExpected with the pointer. The mExpected data member is std::string, which knows how to initialize itself from a pointer.

The pass method also accepts std::string, which will let it compare against actual strings or char pointers. Additionally, the to_string method can return mExpected directly since it’s already a string.

When we were adding more classical confirmations in Chapter 5, Adding More Confirm Types, we added special support for floating point types. We’ll need to add special support for confirming floating-point types in the Hamcrest style, too. The Hamcrest floating-point specializations will come in the next chapter along with learning how to write custom matchers.

Summary

We used TDD throughout this chapter to add Hamcrest confirmations and even improve the existing code for classical confirmations. Without TDD, the existing code in a real project would likely not get approval from management to make changes.

This chapter showed you the benefits of having unit tests that can help verify the quality of code after making changes. We were able to refactor the existing classical confirmations design for dealing with strings so that it matches the new design, which has a similar need. This lets both the classical and Hamcrest confirmations share a similar design instead of maintaining two different designs. All the changes were possible because the unit tests verified that everything continued to run as expected.

The most important changes in this chapter added Hamcrest style confirmations, which are more intuitive and more flexible than the classic confirmations developed in Chapter 4, Adding Tests to a Project. Additionally, the new Hamcrest confirmations are extensible.

We added support for Hamcrest confirmations following a TDD approach, which let us start simply. The simplicity was critical because we soon got into more advanced template specializations and even a new C++20 feature, called requires, that lets us specify how the templates should be used.

TDD makes the process of designing software flow better – from simple ideas at the start of a project or the beginning of an enhancement to an enhanced solution like this chapter developed. Even though we have working Hamcrest confirmations, we’re not done yet. We’ll continue to enhance the confirmations in the next chapter by making sure we can confirm floating-point values and custom-type values.

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

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