3
\$\begingroup\$

Parts

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)...);
}
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.