trompeloeil icon indicating copy to clipboard operation
trompeloeil copied to clipboard

Allow convenient mocking of signals and slots (callback functions)

Open rcdailey opened this issue 6 years ago • 5 comments

I have a few interfaces that allow you to subscribe to events that are published. Usually these are implemented with boost::signals2::signal. For example (using the Catch2 test library):

TEST_CASE("blah")
{
    MyTestObject obj;

    bool callbackInvoked = false;
    obj.RegisterForSuccess([&] { callbackInvoked = true; });
    obj.PerformAction(); // this results in the callback above being invoked
    CHECK(callbackInvoked);
}

Above, a callback is registered that sets a captured boolean to true if it is invoked. In this test, the goal is to ensure that callback is invoked. The means to which I verify this involves a lot of boilerplate code, which I don't really like. This quickly adds up and makes the test cases difficult to maintain and understand when you have a lot of logic that relies on callback mechanisms like this.

If the callback instead were a simple virtual function that was invoked in some interface, I could use a Trompeloeil to verify that function is called. The elegant syntax provided by trompeloeil makes this much easier and better. The cookbook documents a way to mock free functions, but this is very tedious for my use case.

This is just pseudocode, but it would be great to do it like this:

TEST_CASE("blah")
{
    // Simulates a slot with signature `void slot(int)`
    MockCallback<void, int> slot;

    // `OnInvoked()` is a mock method with the signature provided to the class template variadic args
    REQUIRE_CALL(slot, OnInvoked(_))
        .WITH(_1 == 100);

    RealObjectUnderTest obj;

    // Internally, returns an std::bind() with the number of placeholder arguments
    // previously passed to the variadic template parameter
    obj.RegisterForEvent(slot.MakeBind());

    obj.DoStuff(); // eventually causes the callback to be invoked
}

I think it's possible to create a utility class named MockCallback that does this, however the TROMPELOEIL_MAKE_MOCK_ macro's num parameter must be a literal integer, since the macro does concatenation. So I can't use something like sizeof...(Args).

Is this a reasonable idea to you? And if so, any ideas on how this can be implemented?

rcdailey avatar Apr 02 '19 20:04 rcdailey

Maybe I misunderstand, but I think you can already do what you want. Here's a (totally untested) take on how MockCallback<> could look like.

template <typename R, typename ... Args>
struct MockCallback
{
  R OnInvoked(Args... args)
  {
    return InvokedWith(std::tuple<Args>(std::forward<Args>(args)));
  }
  MAKE_MOCK1(InvokedWith, R(std::tuple<Args>));
  auto MakeBind()
  {
    return [this](auto&& ... args) {
      return OnInvoked(std::forward<decltype(args)>(args)...);
    }
  }
};

By using the indirection to pack all arguments in a tuple, you get around the unknown number of arguments in the mock function. This means you have to place the expectation on InvokedWith() instead of OnInvoked(), but I presume that's a minor inconvenience.

Personally I'd prefer to express the type of the callback using a type signature:

template <typename>
struct MockCallback;

template <typename R, typename ... Args>
struct MockCallback<R(Args...)>
{
  ...
};

But that's an unrelated detail.

rollbear avatar Apr 03 '19 05:04 rollbear

Great idea, I will try out what you have. Would be nice to incorporate it into Trompeloeil though, as a contrib component or something. It's a useful utility class. And I would love to have it out of the box on every new project I use your library in!

How do the template arguments change if you use the void(int) function type style?

rcdailey avatar Apr 03 '19 15:04 rcdailey

The usage of the tuple makes some of the Trompeloeil code break due to ambiguity (tuple vs parameter pack). Here's a live sample you can compile.

Error is:

error: conversion from 'const trompeloeil::wildcard' to '::trompeloeil::param_list_t<void (std::tuple<int &&>), 0>' (aka 'std::__1::tuple<int &&>') is ambiguous
    REQUIRE_CALL(cb, OnInvoked(_))
                               ^

rcdailey avatar Apr 03 '19 18:04 rcdailey

That one seems to be a bug. Accepting the tuple by const& works fine. I'll have to look into this.

Here's an example: https://gcc.godbolt.org/z/nDHTtx

Using the function signature style of template parameter makes for a more natural syntax, IMO. Your example would be:

    MockCallback<void(int)> slot;

rollbear avatar Apr 04 '19 06:04 rollbear

Created issue #129 for this, in case you want to follow it.

rollbear avatar Apr 04 '19 06:04 rollbear