Parts
- C++ Mock Library: Part 1
- C++ Mock Library: Part 2
- C++ Mock Library: Part 3
- C++ Mock Library: Part 4
- C++ Mock Library: Part 5
- C++ Mock Library: Part 6
Description:
In part 3 I described how I expect the Unit Test to be constructed. This article I am jumping to the other end. This is the object that will act as the mock implementation.
Helper Classes
// Standardize: Parameter Type for storage.
template<typename P>
struct StandardParameterTypeExtractor
{
// By default I want to use the same type as the function
using Type = P;
};
template<>
struct StandardParameterTypeExtractor<char const*>
{
// But for the special case of C-String
// We will store stuff as C++ std::string
// This makes comparisons really easy
using Type = std::string;
};
template<typename P>
using StandardParameter = typename StandardParameterTypeExtractor<P>::Type;
// ---------
// Convert Args... into a std::tuple<> but replace the
// C-Strings with C++ std::string
template<typename F>
struct ParameterTypeExtractor;
template<typename R, typename... Args>
struct ParameterTypeExtractor<R(Args...)>
{
using Type = std::tuple<StandardParameter<Args>...>;
};
template<typename F>
using ParameterType = typename ParameterTypeExtractor<F>::Type;
// ---------------
// ---------------------
template<typename R>
struct OutputTypeExtractor
{
// Most return types we simply store as the return type.
using Type = R;
};
template<>
struct OutputTypeExtractor<void>
{
// For the void type we are going to store a bool.
// This makes the templated code a lot simpler as we don't extra specializations.
// Note: trying to use .toReturn(false) on a function that returns void will fail
// to compile. This is simply for defining the storage inside the
// class MockResultHolder
using Type = bool;
};
template<typename R>
using OutputType = typename OutputTypeExtractor<R>::Type;
MockResultHolder
// R: Mock function return type
// Args: Mock function input parameters
template<typename R, typename... Args>
class MockResultHolder<R(Args...)>
{
public:
// Useful to have these simply available.
using Ret = OutputType<R>;
using Param = ParameterType<R(Args...)>;
private:
// The swapping of mock with saved lambda is tightly bound.
// This simplifies the code (and announces the relationship).
friend class MockFunctionOveride<R(Args...)>;
// We store all the information about a call here:
using Expected = std::tuple<std::size_t, // Call order (or -1)
std::size_t, // Call count
bool, // Last ordered call of the block
std::optional<Ret>, // The value to return
std::optional<Param>, // Expected Input values.
Required // Is the text requried to call this function
>;
std::string name; // name of mock function (makes debugging simpler)
std::function<R(Args...)> original; // The last override (if somebody used MOCK_SYS() then we keep a reference here.
std::vector<Expected> expected; // The list of calls we can expect.
std::size_t next; // Current location in "expected"
bool extraMode; // Has extraMode been entered.
public:
template<typename F>
MockResultHolder(std::string const& name, F&& original);
std::string const& getName() const;
// Add a call to the expected array
std::size_t setUpExpectedCall(Action action,
std::size_t index, // The id of the next expected call: See Expected
std::size_t count, // The number of times this row is used. See Expected
bool last, // Is this the last ordered call in this block
std::optional<Ret>&& value, // The value to return
std::optional<Param>&& input, // The expected input
Required required, // Must this call be made?
Order order // Is this call ordered? (if so index will be incremented and that value returned).
);
// Remove all calls to the expected array
void tearDownExpectedCall(bool checkUsage);
// The mock call entry point.
R call(TA_Test& parent, Args&&... args);
};
One of these objects exists for every mocked function. They are created and named automatically.
Implementation
// -------------------------
// MockResultHolder
// -------------------------
template<typename R, typename... Args>
template<typename F>
MockResultHolder<R(Args...)>::MockResultHolder(std::string const& name, F&& original)
: name(name)
, original(std::move(original))
, next(0)
, extraMode(false)
{}
template<typename R, typename... Args>
std::string const& MockResultHolder<R(Args...)>::getName() const
{
return name;
}
template<typename R, typename... Args>
std::size_t MockResultHolder<R(Args...)>::setUpExpectedCall(Action action, std::size_t index, std::size_t count, bool last, std::optional<Ret>&& value, std::optional<Param>&& input, Required required, Order order)
{
if (action == AddExtra && !extraMode) {
expected.clear();
extraMode = true;
}
std::size_t indexOrder = -1;
if (order == Order::InOrder) {
indexOrder = index;
index += count;
}
expected.emplace_back(Expected{indexOrder, count, last, std::move(value), std::move(input), required});
return index;
}
template<typename R, typename... Args>
void MockResultHolder<R(Args...)>::tearDownExpectedCall(bool checkUsage)
{
if (checkUsage)
{
std::size_t count = 0;
for (std::size_t loop = next; loop < expected.size(); ++loop) {
Expected& expectedInfo = expected[loop];
Required& required = std::get<5>(expectedInfo);
count += (required == Required::Yes) ? 1: 0;
}
EXPECT_EQ(count, 0)
<< "Function: " << getName() << " did not use all expected calls. "
<< (expected.size() - next) << " left unused";
}
expected.clear();
next = 0;
extraMode = false;
}
template<typename R, typename... Args>
R MockResultHolder<R(Args...)>::call(TA_Test& parent, Args&&... args)
{
//std::cerr << "Calling: " << getName() << "\n";
// If we don't have any queued calls
// The check in with the test object (TA_Test (see part 5))
// This may move us to the next object in the test stack
// (up Init), (down Dest),
// or if extra code has been added/injected that is now examined.
while (next == expected.size()) {
if (!parent.unexpected()) {
// There are no more changes that can be made.
break;
}
}
// We have some queued calls that we should return a value for
if (next < expected.size()) {
Expected& expectedInfo = expected[next];
std::size_t nextCallIndex = std::get<0>(expectedInfo);
std::size_t& callCount = std::get<1>(expectedInfo);
bool lastCall = std::get<2>(expectedInfo);
std::optional<Param>& input = std::get<4>(expectedInfo);
// decrement the count and move to the next if required.
--callCount;
if (callCount == 0) {
++next;
}
// Check with the parent (TA_Test) that the function was called in the
// Correct order.
EXPECT_TRUE(parent.mockCalled(nextCallIndex, lastCall)) << "Function: " << getName() << "Called out of order";
// If there are input values defined then check they
// are what is expected. Note this is a very easy tuple compare test.
if (input.has_value()) {
EXPECT_EQ(input.value(), std::make_tuple(args...));
}
// Note: constexpr
// If the function has a void return type then simply return.
if constexpr (std::is_same_v<R, void>) {
return;
}
else {
// Otherwise we see if there was a stored value.
// return that value.
std::optional<R>& resultOpt = std::get<3>(expectedInfo);
if (resultOpt.has_value()) {
return resultOpt.value();
}
// Otherwise we return a default constructed object.
// For fundamental types this will zero initialize the return value.
return {};
}
}
// We were not meant to get here.
// So indicate an error by throwing.
EXPECT_TRUE(next < expected.size()) << "Function: " << getName() << " called more times than expected";
throw std::runtime_error("Failed");
// return original(std::forward<Args>(args)...);
}