Skip to content

Latest commit

 

History

History
708 lines (535 loc) · 27.2 KB

File metadata and controls

708 lines (535 loc) · 27.2 KB

To Kill A Mocking Framework

Callbacks Are Everywhere!

auto square_add = [] (auto a, auto b) {
  return a * a + b;
};
int is[] = { 1, 2, 3 };
auto q = std::accumulate(std::begin(is), std::end(is), 0, square_add);

Testing

Testing challenges?

Tokenizer Version 1

template <typename callback_t>
struct tokenizer1 {
  callback_t& callback_;
  tokenizer1(callback_t& callback) : callback_{callback} {}

  void operator()(std::string_view input) const {
    const char ws[] = " \t\n";
    size_t a{}, b{};
    do {
      a = input.find_first_not_of(ws, b);
      if (a == input.npos) return;
      b = input.find_first_of(ws, a);
      auto tok = input.substr(a, b == input.npos? b : b - a);
      // call callback here!
      callback_.string_token(tok);
    } while (b != input.npos);
  }
};

Testing Tokenizer

BOOST_AUTO_TEST_CASE(tokenizer_test) {
  /* what type goes here?? */ mock_callback;
  tokenizer1 test_me{mock_callback};
  test_me("hello");
  /* how do we check that the callback is called? */
}

Manual Mocking

One way to solve this problem is to write a mock object for each test case. So for example:

BOOST_AUTO_TEST_CASE(tokenizer_test_manual_mock) {
  struct mock_callback {
    int calls = 0;
    void string_token(std::string_view seen) {
      ++calls;
      BOOST_TEST(seen == "hello");
    }
  } callback;
  tokenizer1 tok {callback};
  tok("hello");
  BOOST_TEST(callback.calls == 1);
}

Mocking Frameworks

Mocking frameworks exist to make mocking easier. Google Mock is one such framework, and here’s how you would solve the problem using that:

struct tokenizer1_test : ::testing::Test {
  struct mock_callback {
    MOCK_METHOD1(string_token, void(std::string_view));
  };
  mock_callback callback_;
  tokenizer1<mock_callback> tokenizer_{callback_};
};

TEST_F(tokenizer1_test, hello) {
  EXPECT_CALL(callback_, string_token("hello"sv)).Times(1);
  tokenizer_("world");
}

Google Test Error

[ RUN      ] tokenizer1_test.hello
unknown file: Failure

Unexpected mock function call - returning directly.
    Function call: string_token({ 'w' (119, 0x77), 'o' (111, 0x6F), 'r' (114, 0x72), 'l' (108, 0x6C), 'd' (100, 0x64) })
Google Mock tried the following 1 expectation, but it didn't match:

../tokenizer1_gtest.cpp:16: EXPECT_CALL(callback_, string_token("hello"sv))...
  Expected arg #0: is equal to { 'h' (104, 0x68), 'e' (101, 0x65), 'l' (108, 0x6C), 'l' (108, 0x6C), 'o' (111, 0x6F) }
           Actual: { 'w' (119, 0x77), 'o' (111, 0x6F), 'r' (114, 0x72), 'l' (108, 0x6C), 'd' (100, 0x64) }
         Expected: to be called once
           Actual: never called - unsatisfied and active
../tokenizer1_gtest.cpp:16: Failure
Actual function call count doesn't match EXPECT_CALL(callback_, string_token("hello"sv))...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] tokenizer1_test.hello (0 ms)

Mocking with std::function

struct tokenizer1_fixture {
  struct mock_callback {
    std::function<void(std::string_view)> string_token;
  };
  mock_callback callback_;
  tokenizer1<mock_callback> tokenizer_{callback_};
};

BOOST_FIXTURE_TEST_CASE(hello_counted, tokenizer1_fixture) {
  int calls = 0;
  callback_.string_token = [&] (std::string_view seen) {
    ++calls;
    BOOST_TEST(seen == "hello");
  };
  tokenizer_("   hello    ");
  BOOST_TEST(calls == 1);
}

Limitations of mocking with std::function 1

No overloading!

struct mock_callback {
  std::function<void(std::string_view)> token_string_view_;
  std::function<void(int)> token_int_;

  void token(std::string_view s) { token_string_view_(s); }
  void token(int i) { token_int_(i); }
};

Limitations of mocking with std::function 2

No overriding!

struct callback_interface {
  virtual void string_token(std::string_view) = 0;
};
struct mock_callback : callback_interface {
  std::function<void(std::string_view)> string_token_fn_;
  void string_token(std::string_view arg) override { string_token_fn_(arg); }
};

Limitations of mocking with std::function 3

Duplication of function signatures

struct {
   std::function<void(std::string_view, int, double) foo;
} callback;

callback.foo = [&] (std::string_view s, int i, double d) { /* */ };

Decorated Callable

template <typename decorator_t, typename callable_t>
struct decorated_callable {
  decorator_t decorator_;
  callable_t callable_;

  template <typename ... args_t>
  auto operator() (args_t&& ... args) {
    decorator_(std::forward<args_t>(args)...);
    return callable_(std::forward<args_t>(args)...);
  }
};

Call Count Checker

// this code is flawed - for exposition only
struct call_count_checker {
  int calls_;
  int expected_;
  template <typename ... args_t>
  void operator() (args_t&&...) {
    ++calls_;
  }
  ~call_count_checker() {
    BOOST_TEST(calls_ == expected_);
  }
};

Using Call Count Checker

template <typename Fn>
auto expect_calls(int expected, Fn fn) {
  return decorated_callable<call_count_checker, Fn>{{0, expected}, fn};
}

BOOST_AUTO_TEST_CASE(hello_expect_calls) {
  callback_.string_token = expect_calls(1, [] (std::string_view seen) {
      BOOST_TEST(seen == "hello");
    });
  tokenizer_("hello");
}

Call Count Checker Take 2

using source_location = std::experimental::source_location;

// nested class within call_count_checker
struct state {
  state(source_location&& location, unsigned expected)
    : location_{std::move(location)}, expected_{expected}
  {}
  ~state() {
    BOOST_TEST(expected_ == calls_,
      "Function defined at " << location_.file_name() << ':' << location_.line()
      << " expected " << *expected_ << " calls, " << calls_ << " seen");
    }
  }
  const source_location location_;
  const unsigned expected_;
  unsigned calls_ {};
};
class call_count_checker {
  struct state { /* ... */  };
  std::shared_ptr<state> state_;
public:
  call_count_checker(unsigned expected, source_location&& location
                     = source_location::current())
    : state_{std::make_shared<state>(std::move(location), expected)}
  {}
  call_count_checker(const call_count_checker&) = default;
  call_count_checker(call_count_checker&&) = default;

  template <typename ... Args>
  void operator() (Args&& ...) const {
    state_->calls_ += 1;
  }
};

Call Count Checker Error

BOOST_AUTO_TEST_CASE(hello_expect_calls_fail, *boost::unit_test::expected_failures(1)) {
  callback_.string_token = expect_calls(1, [] (std::string_view) {});
  tokenizer_("");
}
../mocking.hpp(75): error: in "tokenizer1_test/hello_expect_calls_fail": Function defined at ../tokenizer1_test2.cpp:43 expected 1 calls, 0 seen

Varying expected values

BOOST_AUTO_TEST_CASE(hello_world_1) {
  auto expect_world = expect_calls(1, [&] (std::string_view seen) {
      BOOST_TEST(seen == "world");
    });
  auto expect_hello = expect_calls(1, [&] (std::string_view seen) {
      BOOST_TEST(seen == "hello");
      callback_.string_token = expect_world;
    });
  callback_.string_token = expect_hello;
  tokenizer_("hello world");
}

Multiple Expected Values

BOOST_AUTO_TEST_CASE(hello_world_2) {
  const char* exp_toks[] = { "hello", "world" };
  callback_.string_token = expect_calls(2,
    [exp=exp_toks] (std::string_view seen) mutable {
      BOOST_TEST(seen == *exp++);
    });
  tokenizer_("hello world");
}

Unique Tokenizer

/// calls callback with each unique token
template <typename callback_t>
struct unique_tokenizer {
  callback_t& callback_;

  void operator()(std::string_view input) const {
    struct {
      std::unordered_set<std::string_view> tokens_;
      void string_token(std::string_view word) {
        tokens_.insert(word);
      }
    } uniqifier;
    tokenizer1 tok {uniqifier};
    tok(input);
    for (const auto& t : uniqifier.counts_) {
      callback_.string_token(t);
    }
  }
};

Testing Unique Tokenizer

BOOST_AUTO_TEST_CASE(hello_world) {
  std::set expected { "hello"sv, "world"sv };
  callback_.string_token = expect_calls(2, [&] (std::string_view tok) {
      BOOST_TEST(expected.erase(tok) == 1, word << " not expected");
    });
  tokenizer_("hello world");
}

Tokenizer Version 2

template <typename callback_t>
struct tokenizer2 {
  callback_t& callback_;
  tokenizer2(callback_t& c) : callback_{c} {}

  void operator()(std::string_view input) const {
    const char ws[] = " \t\n";
    size_t a{}, b{};
    do {
      a = input.find_first_not_of(ws, b);
      if (a == input.npos) return;
      b = input.find_first_of(ws, a);
      auto tok = input.substr(a, b == input.npos? b : b - a);
      if (int i; std::from_chars(tok.begin(), tok.end(), i).ec == std::errc{}) {
        callback_.int_token(i);
      } else {
        callback_.string_token(tok);
      }
    } while (b != input.npos);
  }
};

Testing Tokenizer Version 2

BOOST_AUTO_TEST_CASE(hello_123) {
  callback_.string_token = expect_calls(1, [] (std::string_view seen) {
      BOOST_TEST(seen == "hello");
    });
  callback_.int_token = expect_calls(1, [] (int seen) {
      BOOST_TEST(seen == 123);
    });
  tokenizer_("hello 123");
}

Accessing Call Count Checker?

(Possible diagram goes here)

Counted Callable Version 2

struct state {
  state(source_location&& location, std::optional<unsigned> expected)
    : location_{std::move(location)}
    , expected_{expected}
  {}
  ~state() {
    if (expected_) {
      BOOST_TEST(*expected_ == calls_, "Function defined at "
                 << location_.file_name() << ':' << location_.line()
                 << " expected " << *expected_ << " calls, " << calls_ << " seen");
    }
  }
  const source_location location_;
  const std::optional<unsigned> expected_;
  unsigned calls_ {};
};

Counted Function

template <typename>
struct counted_function;

template <typename Ret, typename ... Args>
struct counted_function<Ret(Args...)>
  : decorated_callable<call_count_checker, std::function<Ret(Args...)>>
{
  using base_t = decorated_callable<call_count_checker, std::function<Ret(Args...)>>;

  using base_t::decorated_callable;
  using base_t::operator=;
  using base_t::decorator;

  auto current_count() const { return decorator().current_count(); }
};

Putting it All Together 1

struct tokenizer2_fixture {
  struct mock_callback {
    counted_function<void(std::string_view)> string_token;
    counted_function<void(int)> int_token;
  };

  mock_callback callback_;
  tokenizer2<mock_callback> tokenizer_{callback_};
};

Putting it All Together 2

BOOST_AUTO_TEST_CASE(hello_123_ordered) {
  callback_.string_token = expect_calls(1, [] (std::string_view seen) {
      BOOST_TEST(seen == "hello");
    });
  callback_.int_token = expect_calls(1, [&] (int seen) {
      BOOST_TEST(callback_.string_token.current_count() == 1);
      BOOST_TEST(seen == 123);
    });
  tokenizer_("hello 123");
}

Another sequencing option

BOOST_AUTO_TEST_CASE(hello_123_variant) {
  std::deque<std::variant<std::string_view, int>> exp {{ "hello"sv, 123 }};

  auto check_token = expect_calls(size(exp), [&] (auto seen) {
    auto* next_exp = std::get_if<decltype(seen)>(&exp.front());
    BOOST_REQUIRE(next_exp);
    BOOST_TEST(*next_exp == seen);
    exp.pop_front();
    });
  callback_.int_token = check_token;
  callback_.string_token = check_token;
  tokenizer_("hello 123");
}

Further Enhancements

Come talk to me about

  • Overloaded functions
  • Mocking out the system clock

Further Reading

Contact Details