trompeloeil
trompeloeil copied to clipboard
Allow convenient mocking of signals and slots (callback functions)
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?
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.
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?
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(_))
^
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;
Created issue #129 for this, in case you want to follow it.