Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow convenient mocking of signals and slots (callback functions) #128

Open
rcdailey opened this issue Apr 2, 2019 · 5 comments
Open

Comments

@rcdailey
Copy link
Contributor

rcdailey commented Apr 2, 2019

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?

@rollbear
Copy link
Owner

rollbear commented Apr 3, 2019

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.

@rcdailey
Copy link
Contributor Author

rcdailey commented Apr 3, 2019

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
Copy link
Contributor Author

rcdailey commented Apr 3, 2019

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(_))
                               ^

@rollbear
Copy link
Owner

rollbear commented Apr 4, 2019

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
Copy link
Owner

rollbear commented Apr 4, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants