1
\$\begingroup\$

Parts

Overview

So brief overview:

Part3 was the wrappers for the user of the mocks (long chains of simple objects that collapsed after use) but recorded expected usage, then part 4 was the a slightly more complex object for holding all the information collected in part 3 to check real time calls and validate them.

Part 5 deals with how the information gets from part 3 to part 4. This is the runtime handling of the TA_Object information.

How it works.

This is the task of the TA_Test object (its implementations of TA_TestThrow and TA_TestNoThrow).

For each object we want to track the calls in (Constructor/Destructor) we push a TA_Object (Part 3) object onto a stack (of TA_Test). A method call is the same TA_Object its just that the destructor section is empty. Then at runtime we take the first item in the stack an load the expected calls into the MockResultHolder (Part 4). We then track the function calls as they happen. If any mocked function gets a call they were not expecting they call the unexpected() method on the TA_Test object that is wrapping the test code.

What the unexpected() method does depends. If we are still in the Const (Construct) phase then we will move to the next object in the stack, if we are in the Dest (Destruct) phase then we move back an object in the stack. Ie. if we are constructing we see if the next constructor has been entered if we are destructing then we work backwards to the previous set of destructors (as you would expect in C++).

There is one exception to this. If Extra code has been added to the current stack object we use that first, this is to allow for error situations or alternative paths (when non standard parameters are used) to be injected into the object (TA_Object see part 3).

CODE:

    class TA_Test
    {
        enum State {Init, Construct, Destruct, Error, Cleanup, Finished};
        bool                            hasRun;
        std::vector<TA_Object>          codeBlocks;
        std::size_t                     callOrder;
        std::size_t                     next;
        bool                            lastInBlockReached;
        bool                            exceptionSpotted;
        State                           state;

        public:
            TA_Test();
            ~TA_Test();

            TA_Test& expectObject(TA_Object const& object);
            TA_Test& expectCode(TA_Object const& object);

            template<typename MockObject>
            TA_Code<MockObject> expectCall(MockObject& action);
            void run();

        protected:
            bool isExceptionSpotted() const {return exceptionSpotted;}
            bool unexpectedThrow(bool exceptionInProgress);
            virtual void runLambda() = 0;

        private:
            template<typename R, typename... Args>
            friend class MockResultHolder;

            bool unexpected();
            bool mockCalled(std::size_t order, bool last);

        private:
            template<typename MockObject>
            friend class TA_Code;

            bool addInit();
            bool addDest();
            bool addExtraCode();
            void remInit(bool checkForExtraCode, bool removeOpt);
            void remDest(bool checkForExtraCode);
            void remExtraCode();
            bool setDest(bool exceptionInProgress);
    };

// -------------------------
// TA_Test
// -------------------------

TA_Test::TA_Test()
    : hasRun(false)
    , callOrder(0)
    , next(0)
    , lastInBlockReached(true)
    , exceptionSpotted(false)
    , state(Init)
{}

// Adding the TA_Object
TA_Test& TA_Test::expectObject(TA_Object const& object)
{
    codeBlocks.emplace_back(object);
    return *this;
}

// Adding the TA_Object
TA_Test& TA_Test::expectCode(TA_Object const& object)
{
    codeBlocks.emplace_back(object);
    return *this;
}

TA_Test::~TA_Test()
{
    // You created a test but forgot to call run()
    // That could be an issue?
    EXPECT_TRUE(hasRun) << "You created a test. But it was not run";
}

void TA_Test::run()
{
    hasRun = true;
    // Call the unexpected() first time to initiate the mock methods.
    // This will move the state from Init to Construct
    unexpected();

    // Run the test set up by the user
    runLambda();

    // After tests are run
    // we can call unexpected() multiple times.
    // as long as each object in the stack has not calls in the destructor
    // This will validate that we have gone through and checked the destructors.
    // If any destructor has calls then this will stop.
    while (unexpected()) {
        // If this call generates any required calls.
        // Then we will get appropriate error messages generated.
        // No Code here deliberately
    }

    // There should be no objects left on the stack if
    // all the destructors called the mock methods they should have called.
    if (!codeBlocks.empty()) {
        EXPECT_EQ(state, Finished) << "Not all codeBlocks(s) were used in this test";
    }
}

bool TA_Test::mockCalled(std::size_t order, bool last)
{
    // The mocked object calls this method with the order it was expected to have
    // been called (or -1) if it does not care about order.
    if (order == std::size_t(-1)) {
        // This is an optional method.
        return true;
    }

    // If this call order does not match the current call.
    // We have a problem.
    EXPECT_EQ(order, callOrder) << " Functions called in wrong order";
    ++callOrder;
    lastInBlockReached = last;
    return (callOrder - 1) == order;
}

bool TA_Test::setDest(bool exceptionInProgress)
{
    // We have detected an expception in the code.
    // This means the current destructor should not be called.
    // But all other destructors in the stack as we unwind should be called.
    bool result = true;

    if (!exceptionSpotted && exceptionInProgress) {
        exceptionSpotted = true;
        --next;
    }
    if (next > 0) {
        result = addDest();
        state = (next == 0) ? Cleanup : Destruct;
    }
    else {
        state = Finished;
        result = true;
    }
    return result;
}

bool TA_Test::unexpected()
{
    // Check if there is an exception in progress.
    return unexpectedThrow(std::uncaught_exceptions() > 0);
}

bool TA_Test::unexpectedThrow(bool exceptionInProgress)
{
    // Nothing was added to check.
    // So do no checks.
    if (codeBlocks.size() == 0) {
        return false;
    }

    // If there is an unexpected call.
    // but we have some extra planted code.
    // then see if we can use that.
    if (state != Error && next > 0 && next <= codeBlocks.size() && codeBlocks[next-1].hasExtraCode()) {
        state = Error;
        remInit(false, false);
        lastInBlockReached = addExtraCode();
        return true;
    }

    // Check the exception state.
    if (!exceptionSpotted && exceptionInProgress) {
        if (state == Construct || state == Error) {
            EXPECT_EQ(next, codeBlocks.size()) << "Exception happened before all test codeBlocks used";
        }
    }

    switch (state)
    {
        // Called before running user code to init the state.
        case Init:
        {
            //std::cerr << "    Init -> Construct\n";
            state = Construct;
            lastInBlockReached = addInit();
            break;
        }
        // Called as we move from the constructor of one object
        // to the constructor of the next.
        case Construct:
        {
            remInit(true, true);
            if (next < codeBlocks.size()) {
                // Next Constructor
                lastInBlockReached = addInit();
            }
            else {
                // No more blocks.
                // So lets try destructors.
                lastInBlockReached = setDest(exceptionInProgress);
            }
            break;
        }
        // Called as we go back through the destructors
        case Destruct:
        {
            remDest(true);
            lastInBlockReached = addDest();
            if (next == 0) {
                // No more destructors.
                // So enter cleanup phase.
                state = Cleanup;;
            }
            break;
        }
        case Error:
        {
            remExtraCode();
            if (next < codeBlocks.size()) {
                //std::cerr << "    Error -> Construct\n";
                state = Construct;
                lastInBlockReached = addInit();
            }
            else {
                //std::cerr << "    Error -> Destruct\n";
                lastInBlockReached = setDest(exceptionInProgress);
            }
            break;
        }
        case Cleanup:
        {
            // Cleanup done.
            // Enter Finish state.
            remDest(true);
            state = Finished;
        }
        case Finished:
        {
            // There is no change in state that would help
            // resolve an unexpected call.
            lastInBlockReached = true;
            return false;
        }
    }
    return true;
}

bool TA_Test::addInit()
{
    // Ask the object to register all its mocks used in constructor.
    return codeBlocks[next++].addInit(callOrder);
}

bool TA_Test::addDest()
{
    // Ask the object to register all its mocks used in destructor.
    return codeBlocks[--next].addDest(callOrder);
}

bool TA_Test::addExtraCode()
{
    // Ask the object to register all its mocks used in the extra code state
    // used for errors and non standard paths.
    return codeBlocks[next-1].addExtraCode(callOrder);
}

void TA_Test::remInit(bool checkForExtraCode, bool removeOpt)
{
    // Ask the object to unregister all its mocks used in constructor.
    codeBlocks[next-1].remInit(checkForExtraCode, removeOpt);
}

void TA_Test::remDest(bool checkForExtraCode)
{
    // Ask the object to unregister all its mocks used in destructor.
    codeBlocks[next].remDest(checkForExtraCode);
}

void TA_Test::remExtraCode()
{
    // Ask the object to unregister all its mocks used in the extra code state
    codeBlocks[next-1].remExtraCode();
}

TA_TestThrow

Used to test code that throws.

    template<typename Throws = std::runtime_error>
    class TA_TestThrow: public TA_Test
    {
        std::function<void()>           lambda;
        MockFunctionGroup               defaultMockObjects;
        public:
            template<typename Lambda>
            TA_TestThrow(Lambda&& lambda);
        private:
            virtual void runLambda();
    };

// -------------------------
// TA_TestThrow
// -------------------------

template<typename Throws>
template<typename Lambda>
TA_TestThrow<Throws>::TA_TestThrow(Lambda&& lambda)
    : TA_Test()
    , lambda(std::forward<Lambda>(lambda))
    , defaultMockObjects(*this)
{}

template<typename Throws>
void TA_TestThrow<Throws>::runLambda()
{
    EXPECT_THROW(lambda(), Throws);
    if (!isExceptionSpotted()) {
        unexpectedThrow(true);
    }
}

TA_TestNoThrow

Used to test code that does not throw.

    class TA_TestNoThrow: public TA_Test
    {
        std::function<void()>           lambda;
        MockFunctionGroup               defaultMockObjects;
        public:
            template<typename Lambda>
            TA_TestNoThrow(Lambda&& lambda);
        private:
            virtual void runLambda();
    };


// -------------------------
// TA_TestNoThrow
// -------------------------

template<typename Lambda>
TA_TestNoThrow::TA_TestNoThrow(Lambda&& lambda)
    : TA_Test()
    , lambda(std::forward<Lambda>(lambda))
    , defaultMockObjects(*this)
{}


void TA_TestNoThrow::runLambda()
{
    EXPECT_NO_THROW(lambda());
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.