Skip to main content
added 82 characters in body
Source Link
Loki Astari
  • 97.7k
  • 5
  • 126
  • 341
Source Link
Loki Astari
  • 97.7k
  • 5
  • 126
  • 341

C++ Mock Library: Part 4

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