Jonathan Boccara's blog

Checking the Values of a Collection in a Unit Test in C++

Published March 3, 2020 - 0 Comments

When writing unit tests, we get to write a lot of code to compare observed results with expected results.

Try this case, get the results, compare them with the expected values. Then try this other case, get the results, and check with their expected value. Then try with this third case, and so on and so forth.

To write expressive code in unit tests, we should write comparisons in a simple way. But when it comes to comparing collections, the standard components (as of C++17) leads to more verbose code than necessary.

In this article we will seek a more concise way to express the simple idea of comparing two collections, and we will go over a specificity of std::initializer_list.

Verbose code

To illustrate the sort of code we get with standard components, let’s say that we would like to unit test the following function:

std::vector<int> times7(std::vector<int> const& numbers)
{
    auto results = std::vector<int>{};
    std::transform(begin(numbers), end(numbers), back_inserter(results), [](int n){ return 7 * n; });
    return results;
}

This function is supposed to take a collection of numbers and multiply them by 7. Granted, this is not the most ingenious function in the world, but the point is just to have a function returning a collection in order to illustrate the case.

In our first unit test, we going to compare the observed values with expected values on a pretty average use case:

auto const inputs = std::vector<int>{3, 4, 7};

auto const results = times7(inputs);
auto const expected = {21, 28, 49};

REQUIRE(std::equal(begin(results), end(results), begin(expected), end(expected)));

(Here REQUIRE is the macro used in the Catch 2 testing framework. If you use GTest, you would have something like EXPECT_TRUE instead.)

This code does the job, but wouldn’t it be nice to be able to write something like this instead?

auto const inputs = std::vector<int>{3, 4, 7};

REQUIRE(times7(inputs) == {21, 28, 49});

This doesn’t make a such a difference for one unit test, but the more test cases the more significant the effect on conciseness.

However, unless we modify the interface of times7 to return something else than a vector (which would be damaging it), I can’t see how to make the above code compile. If you see how, please leave a comment. Instead, the syntax we will implement is this:

auto const inputs = std::vector<int>{3, 4, 7};

REQUIRE(equal(times7(inputs), {21, 28, 49}));

This is not as pretty as operator== but this is still more compact and readable than the initial code.

A range algorithm

To implement the function equal before C++20, we can do the classic trick of reusing the code of the STL:

template<typename Range1, typename Range2>
bool equal(Range1 const& range1, Range2 const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

Let’s now compile the target code:

auto const inputs = std::vector<int>{3, 4, 7};

REQUIRE(equal(times7(inputs), {21, 28, 49}));

And we get… a compile error!

error: no matching function for call to 'equal(std::vector<int>, <brace-enclosed initializer list>)'
 REQUIRE(equal(times7(inputs), {21, 28, 49}));

Why isn’t the equal function called? Range2 is a template parameter that can accept any type, so it should be able to compile with the initializer list that we’re passing it, namely {21, 28, 49}, shouldn’t it?

A surprising thing is that if we declare it on a separate line, it compiles fine:

auto const inputs = std::vector<int>{3, 4, 7};

auto const expected = {21, 28, 49};

REQUIRE(equal(times7(inputs), expected));

Maybe it has something to do with expected being a lvalue and {21, 28, 49} being an rvalue? To be sure, let’s try with an std::vector as an rvalue:

auto const inputs = std::vector<int>{3, 4, 7};

REQUIRE(equal(times7(inputs), std::vector<int>{21, 28, 49}));

This compiles fine. So there must be something specific to the std::initializer_list being created on the statement of the function call.

A specificity of std::initializer_list

What’s going on here? The answer is explained in Effective Modern C++, item 2:

“The treatment of braced initializers is the only way in which auto type deduction and template type deduction differ. When an auto-declared variable is initialized with a braced initializer, the deduced type is an instantiation of std::initializer_list. But if the corresponding template is passed the same initializer, type deduction fails, and the code is rejected.”

Now you may wonder why this is. Scott Meyers goes on to explain:

“You might wonder why auto type deduction has a special rule for braced initializers, but template type deduction does not. I wonder this myself. Alas, I have not been able to find a convincing explanation. But the rule is the rule […].”

Now that we understand the situation, and even if we don’t understand the rationale, how should we fix the equal function to make it accept our code? One way to go about it is to make it accept an std::initializer_list explicitly:

template<typename Range1, typename Value2>
bool equal(Range1 const& range1, std::initializer_list<Value2> const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

With this version of equal, our desired code compiles:

auto const inputs = std::vector<int>{3, 4, 7};

REQUIRE(equal(times7(inputs), {21, 28, 49}));

To be able to pass the initializer list as a first parameter, or two initializer lists, or two other collections, in short, to be able to write all these combinations:

REQUIRE(equal(times7(inputs), {21, 28, 49}));
REQUIRE(equal({21, 28, 49}, times7(inputs)));
REQUIRE(equal(times7(inputs), times7(inputs)));
REQUIRE(equal({21, 28, 49}, {21, 28, 49}));

We need several overloads of equal:

template<typename Range1, typename Value2>
bool equal(Range1 const& range1, std::initializer_list<Value2> const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

template<typename Value1, typename Range2>
bool equal(std::initializer_list<Value1> const& range1, Range2 const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

template<typename Value1, typename Value2>
bool equal(std::initializer_list<Value1> const& range1, std::initializer_list<Value2> const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

template<typename Range1, typename Range2>
bool equal(Range1 const& range1, Range2 const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2), end(range2));
}

This way our equal function compiles for all types of collections.

Final question: is there a way to make some of those overloads call each other, so that we don’t repeat the call to std::equal?

If you know the answer, please tell everyone in the comments section 👇.

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin