Just for fun, I'm trying to create a class, that allows "basic mocking functionality". More concretely, you can set return values for function calls. That's it ;D
I have a solution for that, however I don't really like it. I'll show the code, give an explanation and tell what I don't like about it. I am using C++20.
#include <any>
#include <iostream>
#include <string_view>
#include <typeindex>
#include <unordered_map>
using namespace std;
class MockHelper {
public:
virtual ~MockHelper() = default;
template<typename Class, typename Ret, typename ...Args>
void
setFunctionReturnValue(const string_view &functionName, Ret(Class::*functionPtr)(Args...), const Ret &returnValue) {
functionsReturnValues[functionName][type_index(typeid(functionPtr))] = make_any<Ret>(returnValue);
}
protected:
template<typename Class, typename Ret, typename ...Args>
Ret handleCall(const string_view &functionName, Ret(Class::*functionPtr)(Args...)) {
const auto &returnValues = functionsReturnValues[functionName];
const auto typeIndex = type_index(typeid(functionPtr));
if (!returnValues.contains(typeIndex))
return {};
return any_cast<Ret>(returnValues.at(typeIndex));
}
private:
unordered_map<string_view, unordered_map<type_index, any>> functionsReturnValues;
};
class SomethingToMock : public MockHelper {
public:
virtual int f() {
return handleCall("f", &SomethingToMock::f);
}
virtual int g() {
// g is overloaded, thus we need to provide the template arguments explicitly
return handleCall<SomethingToMock, int>("g", &SomethingToMock::g);
}
int g(bool) {
return handleCall<SomethingToMock, int, bool>("g", &SomethingToMock::g);
}
};
int main() {
SomethingToMock beingMocked;
// Setup expected return values
beingMocked.setFunctionReturnValue("f", &SomethingToMock::f, 0);
// again, g is overloaded, thus we need to provide the template arguments explicitly
beingMocked.setFunctionReturnValue<SomethingToMock, int>("g", &SomethingToMock::g, 1);
beingMocked.setFunctionReturnValue<SomethingToMock, int, bool>("g", &SomethingToMock::g, 2);
// Show that this is actually working
std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;
return 0;
}
class MockHelper
allows to set return values for function calls (via setFunctionReturnValue
), and allows to execute those function calls from derived classes (via handleCall
).
class SomethingToMock
derives from class MockHelper
and contains 3 member functions which I use to show stuff.
In the main
function we setup return values and show that it actually works.
A LIVE DEMO.
I dislike, that I have to give functions names as strings to all relevant functions. This is because if I wouldn't I couldn't distinguish the functions. E.g.
type_index(typeid(&SomethingToMock::f)) == type_index(typeid(static_cast<int (SomethingToMock::*)()>(&SomethingToMock::g)))
evaluates to true
, which means f()
and g()
cannot be distinguished by the signatures only, we have to include the names as well.
So, my question is, can we somehow generate unique keys for all the functions, so that they can be used in e.g. an unordered_map, without having to explicitly give the names of the functions?
So, as an example the main
function should look more like this:
int main() {
SomethingToMock beingMocked;
// Setup expected return values
beingMocked.setFunctionReturnValue(&SomethingToMock::f, 0);
// again, g is overloaded, thus we need to provide the template arguments explicitly
beingMocked.setFunctionReturnValue<SomethingToMock, int>(&SomethingToMock::g, 1);
beingMocked.setFunctionReturnValue<SomethingToMock, int, bool>(&SomethingToMock::g, 2);
// Show that this is actually working
std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;
return 0;
}
It is possible to generate a unique hashable/comparable value at compile-time for each member function. This answer will generate a unique MethodId
value for each member function, defined as:
struct MethodTag {};
using MethodId = MethodTag const*; // hashable, comparable
First, let's define some generic traits and helpers:
// Extracts information from a member function.
template<typename T>
struct MethodTraits {
static constexpr auto isMethod = false;
};
template<typename RetType_, typename Object_, typename... Args_>
struct MethodTraits<RetType_ (Object_::*)(Args_...)> {
static constexpr auto isMethod = true;
using RetType = RetType_; // return type of the member function
using Object = Object_; // "*this" type of the member function
};
// Checks if a type represents a member function.
template<typename T>
concept cMethod = MethodTraits<T>::isMethod;
// Gets the return type of a member function.
template<cMethod auto method>
using ReturnTypeOf = typename MethodTraits<decltype(method)>::RetType;
The unique MethodId
of a member function can simply be generated with:
template<cMethod auto method>
struct UniqueMethodId {
static constexpr auto tag = MethodTag{}; // unique tag for `method`.
};
template<cMethod auto method>
constexpr MethodId uniqueMethodId() {
return &UniqueMethodId<method>::tag;
}
The key point here is that the member function pointer method
(ex: &SomethingToMock::f
) is passed by value, not just by type. This ensures that different member functions get a different MethodId
, even if they have the same arguments/return type.
The original code can then be refactored as:
class MockHelper {
public:
virtual ~MockHelper() = default;
template<cMethod auto method>
void setFunctionReturnValue(ReturnTypeOf<method> const& returnValue) {
using RetType = ReturnTypeOf<method>;
static constexpr auto methodId = uniqueMethodId<method>();
functionsReturnValues[methodId] = std::make_any<RetType>(returnValue);
}
protected:
template<cMethod auto method>
auto handleCall() {
using RetType = ReturnTypeOf<method>;
static constexpr auto methodId = uniqueMethodId<method>();
return std::any_cast<RetType>(functionsReturnValues.at(methodId));
}
private:
std::unordered_map<MethodId, std::any> functionsReturnValues;
};
class SomethingToMock : public MockHelper {
public:
virtual int f() { // virtual methods are fine.
return handleCall<&SomethingToMock::f>();
}
virtual int g() {
using Method = int (SomethingToMock::*)(); // overload resolution
return handleCall<Method{&SomethingToMock::g}>();
}
int g(bool) {
using Method = int (SomethingToMock::*)(bool); // overload resolution
return handleCall<Method{&SomethingToMock::g}>();
}
};
int main() {
SomethingToMock beingMocked;
// Setup expected return values
beingMocked.setFunctionReturnValue<&SomethingToMock::f>(0);
static constexpr int (SomethingToMock::*gVoid)() = &SomethingToMock::g; // overload resolution
beingMocked.setFunctionReturnValue<gVoid>(1);
static constexpr int (SomethingToMock::*gBool)(bool) = &SomethingToMock::g; // overload resolution
beingMocked.setFunctionReturnValue<gBool>(2);
// Show that this is actually working
std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;
return 0;
}