mimic++ v2
Loading...
Searching...
No Matches
mock

The core aspect of the library. More...

Collaboration diagram for mock:

Topics

 interfaces
 Contains utility to simplify interface mocking.
 

Classes

class  mimicpp::Mock< FirstSignature, OtherSignatures >
 A Mock type, which fully supports overload sets. More...
 

Macros

#define MIMICPP_SCOPED_EXPECTATION   MIMICPP_DETAIL_SCOPED_EXPECTATION_IMPL(__COUNTER__)
 Convenience macro, which creates a ScopedExpectation with a unique name.
 
#define SCOPED_EXP   MIMICPP_SCOPED_EXPECTATION
 Shorthand variant of MIMICPP_SCOPED_EXPECTATION.
 

Detailed Description

The core aspect of the library.

Mocks are responsible for providing a convenient interface to set up expectations and handle received calls. At a basic level users can specify arbitrary overload sets, which the mock shall provide and for which expectations can be defined.

Mocks themselves can be used as public members and can therefore serve as member function mocks. Have a look at the following example, which demonstrates how one is able to test a custom stack container adapter (like std::stack) by utilizing mocking.

At first, we define a simple concept, which our mock must satisfy.

template <typename T, typename Value>
concept stack_backend_for = requires(T& container, Value v)
{
{ std::as_const(container).empty() } -> std::convertible_to<bool>;
{ container.back() } -> std::convertible_to<Value&>;
{ std::as_const(container).back() } -> std::convertible_to<const Value&>;
container.push_back(std::move(v));
{ container.pop_back() };
};

The implemented must be test-able for emptiness, must have a push_back and pop_back function and must provide access to the last element (both, const and non-const).

The MyStack implementation is rather simple. It provides a pair of pop and push functions and exposes the top element; as const or non-const reference. pop and both top overloads test whether the inner container is empty and throw conditionally.

template <typename T, stack_backend_for<T> Container>
class MyStack
{
public:
[[nodiscard]]
explicit MyStack(Container container)
: m_Container{std::move(container)}
{
}
[[nodiscard]]
auto& top()
{
if (m_Container.empty())
{
throw std::runtime_error{"Container is empty."};
}
return m_Container.back();
}
[[nodiscard]]
const auto& top() const
{
if (m_Container.empty())
{
throw std::runtime_error{"Container is empty."};
}
return m_Container.back();
}
void push(T obj)
{
m_Container.push_back(std::move(obj));
}
void pop()
{
if (m_Container.empty())
{
throw std::runtime_error{"Container is empty."};
}
m_Container.pop_back();
}
private:
Container m_Container;
};

To make the test simpler, let's fixate T as int. A conforming mock then must provide 5 member functions:

Eventually we are able to formulate our tests. The test for push is rather straight-forward.

SECTION("When element is added.")
{
ContainerMock innerContainer{};
SCOPED_EXP innerContainer.push_back.expect_call(42);
MyStack<int, ContainerMock> stack{std::move(innerContainer)};
stack.push(42);
}

We create our container mock, setting up the expectation and moving it into the actual stack. The move is required, because MyStack doesn't expose the inner container. There are more advanced solutions for that kind of design, but that would be out of scope of this example.

The pop() test is also quite simple, but we should definitely test, that pop never tries to remove an element from an empty container.

SECTION("When top element is removed.")
{
SECTION("Throws, when inner container is empty.")
{
ContainerMock innerContainer{};
SCOPED_EXP innerContainer.empty.expect_call()
and finally::returns(true);
MyStack<int, ContainerMock> stack{std::move(innerContainer)};
REQUIRE_THROWS_AS(stack.pop(), std::runtime_error);
}
SECTION("Succeeds silently, when at least one element exists.")
{
ContainerMock innerContainer{};
SCOPED_EXP innerContainer.empty.expect_call()
and finally::returns(false);
SCOPED_EXP innerContainer.pop_back.expect_call();
MyStack<int, ContainerMock> container{std::move(innerContainer)};
container.pop();
}
}

As you can see, the success test sets up two distinct expectations; They may be satisfied in any order, even if only one order here semantically makes sense. We could use a sequence object here instead, but with the second test (the empty case) we already have enough confidence.

The empty test then just creates a single expectation, but in fact it also implicitly tests, that the pop_back of the container is never called.

Finally, we should test both of the top() functions.

SECTION("When top element is accessed.")
{
ContainerMock innerContainer{};
SECTION("Throws, when inner container is empty.")
{
SCOPED_EXP innerContainer.empty.expect_call()
and expect::times(2) // we test both, the const and non-const top() overload
and finally::returns(true);
MyStack<int, ContainerMock> stack{std::move(innerContainer)};
REQUIRE_THROWS_AS(stack.top(), std::runtime_error);
REQUIRE_THROWS_AS(std::as_const(stack).top(), std::runtime_error);
}
SECTION("Returns a reference to the top element otherwise.")
{
SCOPED_EXP innerContainer.empty.expect_call()
and finally::returns(false);
SECTION("A const-ref, when accessed via const.")
{
SCOPED_EXP std::as_const(innerContainer).back.expect_call()
and finally::returns(42);
MyStack<int, ContainerMock> stack{std::move(innerContainer)};
REQUIRE(42 == std::as_const(stack).top());
}
SECTION("A mutable ref, when accessed via non-const.")
{
SCOPED_EXP innerContainer.back.expect_call()
and finally::returns(42);
MyStack<int, ContainerMock> stack{std::move(innerContainer)};
REQUIRE(42 == stack.top());
}
}
}

In the first section we check both overloads, when no elements are present.

The second section then tests when the container is not empty.

Macro Definition Documentation

◆ MIMICPP_SCOPED_EXPECTATION

#define MIMICPP_SCOPED_EXPECTATION   MIMICPP_DETAIL_SCOPED_EXPECTATION_IMPL(__COUNTER__)

Convenience macro, which creates a ScopedExpectation with a unique name.

◆ SCOPED_EXP

#define SCOPED_EXP   MIMICPP_SCOPED_EXPECTATION

Shorthand variant of MIMICPP_SCOPED_EXPECTATION.