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)...);
}
1 Answer 1
I only have some minor remarks on the implementation.
There are 2 commented-out lines in there:
//std::cerr << "Calling: " << getName() << "\n";
and
// return original(std::forward<Args>(args)...);
Delete all commented out code.
While we're at that last bit:
// 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");
It's a good idea to leave something here in case you do reach it. Considering you've already taken the trouble to leave it here, I'd expect the message to be more helpful. getName()
was called more times than expected, but how much was expected? The variables are already in that line, but don't appear to end up in what's shown to the user. If we get somewhere we're not supposed to get, I'd want to know some details.
Honestly, the rest looks pretty good. You're using the right types, throw at the right times and the order/expectation checking is a nice touch. There are some minor inconsistencies throughout the 3 sections in how your code is presented (comments and (vertical) whitespace), but that's about it. I don't think I have to explain the pros and cons of automatic linters to you, so you've probably reached a conclusion about whether those are worth it to you already.