Search code examples
c++unit-testinggoogletestmatchergooglemock

How to write a good GMock matcher for two-dimensional arrays?


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?


Solution

  • 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).