I needed a limited set of algebra operations on two dimensional matrices for my C++ code. I decided to implement this using std::array
like so:
template <typename T, size_t N, size_t M>
using array_2d = std::array<std::array<T, M>, N>;
How should I properly write a GMock matcher for this type, to compare two such matrices of doubles? I came up with the not so smart:
MATCHER_P(Arrays2dDoubleEq, expected, "") {
for (int i = 0; i < arg.size(); i++) {
for (int j = 0; j < arg[i].size(); j++) {
EXPECT_THAT(arg[i][j], DoubleEq(expected[i][j]));
}
}
return true;
}
MATCHER_P2(Arrays2dDoubleNear, expected, max_abs_err, "") {
for (int i = 0; i < arg.size(); i++) {
for (int j = 0; j < arg[i].size(); j++) {
EXPECT_THAT(arg[i][j], DoubleNear(expected[i][j], max_abs_err));
}
}
return true;
}
Which I use like: EXPECT_THAT(result, Arrays2dDoubleEq(expected));
This not only looks very hard coded, but also doesn't give nice feedback. When matrices mismatch the output of failed assertions is a pile of not equal doubles. The failed test output is hardly readable and lacks info about indexes of matrices.
I feel that it can be done (and should be done) in much better way. I have already looked at some documentation and GMock cookbook. Although there are some matchers for containers I can't make any use of them to compare two nested arrays at once.
Can anyone indicate which GMock functionalities I should use to make this matcher better? Or perhaps someone can point a part of documentation that I should read more carefully to understand what I may be missing here?
One thing you might consider is to explicitly return false
or true
form your matcher instead calling assertions. Then you can use result_listener
to provide additional information about what exactly went wrong during match. You should also check the arrays dimensions before performing the check to avoid undefined bahavior
using testing::DoubleEq;
using testing::Value;
using testing::Not;
MATCHER_P(Arrays2dDoubleEq, expected, "") {
if (arg.size() != expected.size())
{
*result_listener << "arg.size() != expected.size() ";
*result_listener << arg.size() << " vs " << expected.size();
return false;
}
for (size_t i = 0; i < arg.size(); i++) {
if (arg[i].size() != expected[i].size())
{
*result_listener << "arg[i].size() != expected[i].size() i = " << i << "; ";
*result_listener << arg[i].size() << " vs " << expected[i].size();
return false;
}
for (size_t j = 0; j < arg[i].size(); j++) {
if (!Value(arg[i][j], DoubleEq(expected[i][j])))
{
*result_listener << "element(" << i << ", " << j << ") mismatch ";
*result_listener << arg[i][j] << " vs " << expected[i][j];
return false;
}
}
}
return true;
}
TEST(xxx, yyy)
{
array_2d<double, 2, 3> arr1 = {std::array<double, 3>({1, 2, 3}), std::array<double, 3>({4, 5, 6})};
array_2d<double, 2, 3> arr2 = arr1;
array_2d<double, 2, 3> arr3 = arr1;
arr3[0][0] = 69.69;
array_2d<double, 5, 6> arr4;
ASSERT_THAT(arr1, Arrays2dDoubleEq(arr2));
ASSERT_THAT(arr2, Not(Arrays2dDoubleEq(arr3)));
ASSERT_THAT(arr2, Not(Arrays2dDoubleEq(arr4)));
}
Unfortunately, I didn't figure out yet how to tell gmock not to print the contents of std::array
in Value of
and Expected
feedback fields. in the docs they mention void PrintTo
function, but it did not work for me.
=== EDIT ===
If it is OK for you to create a 2D array class instead of typedef, then it is easy to suppress gmock messy output by providing operator<<
overload:
template <typename T, size_t N, size_t M>
struct Array2D
{
std::array<std::array<T, M>, N> data;
};
template <typename T, size_t N, size_t M>
std::ostream& operator<<(std::ostream& os, const Array2D<T, N, M>&)
{
os << "Array2D<" << typeid(T).name() << "," << N << "," << M << ">";
return os;
}
Then you need to modify the matcher a bit to use data
class field instead of operator[]
and size()
directly. Or you can overload them for your class.
If the comment of @JanHackenberg is what you desire, inside your matcher just set flag result = false
instead of return
(I wouldn't do that though, because for big arrays it will not be readable).