Introduction
Writing unit tests for software projects can be challenging — especially when those projects depend on many external components.
A key strategy for building maintainable systems is to minimize coupling to such dependencies.
This often involves the use of interfaces and abstractions, which allow your code to remain flexible and testable.
NOTE In this guide, the term interface refers to a general abstraction and does not necessarily imply a polymorphic class.
We’ll explore the fundamentals of mimic++ using a simple quiz-based toy application: Mock It Right!.
This app provides a hands-on way to learn how to create mocks, define expectations, and test behavior effectively with the framework.
Mock It Right!
Let’s begin by examining the design of our Quiz app, which is fully contained within the namespace mock_it_right.
A quiz consists of multiple items — each represented by a QuizItem.
Each QuizItem holds:
- a
questionstring containing the quiz question, - and a fixed-size array
choiceswith four possible answer options.
Importantly, the correct answer is always stored at index 0 of the choices array.
struct QuizItem
{
std::string question;
// First index is always the correct answer.
std::array<std::string, 4u> choices;
friend bool operator==(QuizItem const&, QuizItem const&) = default;
};
The main logic lives in the Quiz class, which is initialized with a non-empty collection of quiz items:
class Quiz
{
public:
explicit Quiz(std::vector<QuizItem> items) noexcept
: m_Items{std::move(items)}
{
assert(!m_Items.empty() && "No items given.");
}
// ...
private:
std::vector<QuizItem> m_Items;
};
Quiz provides a run method that starts a quiz round.
This method is responsible for:
- displaying each question and its choices to the user,
- gathering user input,
- validating each answer, and
- returning a result summary that reflects the user’s performance.
Because these responsibilities are diverse, we apply the separation of concerns principle by introducing a dedicated I/O context interface.
This abstraction handles input/output operations, enabling Quiz to remain decoupled from specific I/O mechanisms like std::iostream.
To express this I/O contract cleanly, we define a C++20 concept io_context.
It ensures that any compatible type:
- can read a single character of input,
- can display a question string,
- and can print a list of answer choices.
template <typename T>
concept io_context = requires(T& ctx, std::string_view question, std::span<std::string const, 4u> choices){
{ ctx.read() } -> std::convertible_to<char>;
ctx.write(question);
ctx.write(choices);
};
NOTE Not familiar with C++20 concepts yet? No worries! This just means that any implementation must (at minimum) provide these three functions. I’m only using it to show that mimic++ works even when there’s no inheritance or virtual functions involved — something many other mocking frameworks don’t directly support. No need to fully understand the details — and I promise this is the last time we’ll dip this deep into language features.
The Quiz::run method is then templated over this concept:
template <io_context IOContext>
std::vector<bool> Quiz::run(IOContext ctx) const
{
std::vector<bool> results{};
// (item-processing)
return results;
}
It loops through each quiz item:
template <io_context IOContext>
std::vector<bool> Quiz::run(IOContext ctx) const
{
// ...
for (auto const& item : m_Items)
{
ctx.write(item.question);
ctx.write(item.choices);
int answer = request_answer(ctx);
results.emplace_back(0 == answer);
}
// ...
}
We expect the user to answer with one of the characters a, b, c, or d (case-insensitive).
All other input should be ignored and retried.
This logic is handled by the private helper method read_answer:
template <io_context IOContext>
int Quiz::read_answer(IOContext& ctx) const
{
int answer{-1};
do
{
switch (ctx.read())
{
case 'a': [[fallthrough]];
case 'A':
answer = 0;
break;
case 'b': [[fallthrough]];
case 'B':
answer = 1;
break;
case 'c': [[fallthrough]];
case 'C':
answer = 2;
break;
case 'd': [[fallthrough]];
case 'D':
answer = 3;
break;
}
}
while (answer < 0);
return answer;
}
Implementing a main function that connects all these parts is beyond the scope of this section. If you’re curious, you can see an example here.
Mocks & Expectations
In general, mocks are objects that simulate the behavior of a real implementation to the system under test (SUT), while giving the unit test full control over their responses and behavior.
In mimic++, mocks are regular C++ objects.
They do not require a special framework context and can be constructed anywhere you need them.
Mocks are created using the mimicpp::Mock class template, which accepts one or more function types to define the mock’s callable interface.
For example:
mimicpp::Mock<void ()> myMock{};
This creates a mock object that exposes an operator () with no parameters and void as return type (i.e. void operator()()).
The mock can be invoked like any other function object: myMock();
Calling mocks is only half the story, though. By default, all calls to a mock are rejected unless explicitly allowed. This is where expectations come in.
Expectations define how and when a mock may be called, and what should happen in response. They are typically defined on a per-test-case basis.
Unlike some other mocking frameworks, mimic++ enforces a strict expectation strategy. Every mock call must match an existing expectation. Otherwise, the call will be rejected, a violation will be reported, and the test will be aborted.
Expectations are also scope-based: they are verified during their destruction. If an expectation is not fulfilled (i.e., not satisfied), it triggers a violation at the end of its lifetime.
We’ll explore how to define and manage expectations in the next section.
A first test
Let’s now attempt to write our first unit test for the Quiz class.
Before diving in, let’s briefly recap what Quiz does:
- Accepts a non-empty collection of
QuizItemobjects. - Uses a provided
IOContextto print questions and answer choices to the user. - Reads the user’s input from that same context.
This means we have
- a fixed input (the quiz items), and
- an interface (the
IOContext), which — totally by coincidence 🙈 — is a perfect target for mocking.
Test Strategy
We’ll write a test that
- sets up a quiz with one question,
- simulates a user answering it and
- verifies whether the
Quiz::runresult is correct.
Here’s the skeleton of our test:
TEST_CASE("Quiz determines the correctness of the given answers.")
{
mock_it_right::Quiz const quiz{
{QuizItem{.question = "Q1", .choices = { "A1", "A2", "A3", "A4"}}}};
auto const [expectedCorrectness, answer] = GENERATE(
table<bool, char>({
{false, 'B'},
{false, 'c'},
{false, 'd'},
{true, 'A'},
{true, 'a'}
}));
// (1) ...
std::vector const result = quiz.run(ioCtx);
REQUIRE(1u == result.size());
CHECK(expectedCorrectness == result.front());
}
This single test case actually runs five times, using the Catch2 GENERATE macro.
Three of the inputs are wrong answers (B, c, d), and two are correct (A, a).
TEST_CASEdefines a test function.GENERATEallows for parameterized test inputs — this test runs once for each row of the table.REQUIREis a fatal assertion — it stops the test immediately if it fails.CHECKis a non-fatal assertion — it reports a failure but continues running the test.
The missing part // (1) ... is where we’ll create a mock ioCtx and set up expectations to simulate the correct I/O behavior.
The first Mock type
Let’s take a look at how we can define an appropriate type for ioCtx, which provides a read method and two write method overloads.
Let’s call it IOContextMock:
struct IOContextMock
{
mimicpp::Mock<char ()> read{};
mimicpp::Mock<
void (std::string_view), // the first overload
void (std::span<std::string_view const, 4u>) // the second overload
> write{};
};
As already stated, mimicpp::Mock supports overloading, which you can see in action in the write method case.
NOTE As mocks are just invocable objects, we exploit them here as member functions. It’s worth noting, that they are — from the language perspective — not actual functions; they are still just (invocable) member objects. Nevertheless, the user of such a class won’t notice any difference when calling them, except when they try to obtain a member function pointer to them, which is not possible.
mimicpp::Mock<Signatures+>,
where Signatures+ is replaced by the list of overloads.
You can override this by explicitly assigning a name to the mock instance.
For example:
mimicpp::Mock<char ()> read{{.name = "IOContextMock::read"}};
The first Expectation
Setting up expectations for a mock is a relatively straightforward process.
Let’s begin by creating an expectation for the question text, which is provided to the IOContext::write(std::string_view) overload.
IOContextMock ioCtx{}; // Create the mock object just as a standalone-mock by simply default constructing it.
mimicpp::ScopedExpectation questionTextExp = ioCtx.write.expect_call("Q1");
As you can see, creating an expectation is simply a matter of calling .expect_call on the mock object with the correct arguments.
Let’s now examine what correct actually means in this context.
Introduction to Matchers
NOTE In the following, I’ll refer to several sub-namespaces such as matches.
They’re actually defined inside the mimicpp namespace (e.g. mimicpp::matches),
but I recommend aliasing them into the global namespace.
This keeps your expectation-setup code concise and much more readable.
expect_call requires the same number of arguments as the corresponding operator(), but the argument types do not necessarily have to match exactly.
In fact, the arguments passed to expect_call are so-called matchers, which are responsible for verifying whether the given input is acceptable.
In general, matchers must be provided explicitly (e.g., myMock.expect_call(mimicpp::eq(42)), which expects the first argument to be equal to 42).
However, mimic++ supports a more concise syntax for the equality case, as used in our write expectation above.
So, the more verbose equivalent of our example would be:
ioCtx.write.expect_call(matches::str::eq("Q1")), which simply states that the first argument must equal the string "Q1".
matches::str::eqis used when the parameter is a string type,matches::eqis used when the parameter is equality-comparable,- and it is rejected with a compile-error, when neither condition applies.
Managing Expectation-Lifetimes
Now let’s focus on the left-hand side of that expression:mimicpp::ScopedExpectation questionTextExp
ScopedExpectation is a closure type that stores the generated expectation and verifies it upon destruction.
Since C++ (prior to C++26) doesn’t allow keeping objects with a
placeholder-name,
this often results in more verbose syntax than desired.
In general, it’s not necessary to have access to an expectation object, thus always be required to name it uniquely is just noise.
To simplify this, mimic++ provides the macro SCOPED_EXP(or its longer alias MIMICPP_SCOPED_EXPECTATION),
which handles creation and naming automatically.
Our example can therefore be simplified to: SCOPED_EXP ioCtx.write.expect_call("Q1");
Building up more Expectations
Now let’s set up the .write call for the corresponding choices.
This time, we’ll provide an explicit matcher by hand: matches::range::eq, which requires component-wise equality.
SCOPED_EXP ioCtx.write.expect_call(matches::range::eq(std::array{"A1", "A2", "A3", "A4"}));
Your container type needn’t match the parameter type exactly — just ensure its elements are comparable for equality with the parameter container-type elements.
Expectations and their Policies
Finally, let’s tackle the .read case by returning our test’s answer variable.
Previous expectations were trivial — they only required minimal setup and just returned void.
This time, our mock must return a value (char), so mimic++ requires you to specify a return value when setting up the expectation.
Here’s where the so-called expectation-policies come into play:
they let you tweak an expectation’s behavior in various ways, giving you powerful control over multiple aspects.
NOTE expectation-policies is the umbrella term for features that modify expectation behavior. Several policy categories exist, each affecting different aspects of an expectation.
Because our mock must return a char, we use the finally::returns (finalizer-)policy.
This finalizer takes a value and returns it whenever the expectation is matched.
SCOPED_EXP ioCtx.read.expect_call()
and finally::returns(answer);
Notice how you chain policies simply by using the overloaded operator and (which is just an alias for operator &&).
NOTE During the design process, I aimed to make the expectation-setup code as readable as possible, without requiring too much knowledge about mimic++ from the readers of the code. Therefore, I introduced several policy sub-namespaces, which then almost form an English sentence for the construction.
Sequencing Expectations
Are we done now? No!
There is an important case missing: namely, the scenario where users enter an invalid answer that does not correspond to the given choices.
While this case is already handled by Quiz::read_input, it definitely needs to be tested.
But how can we do that?
In fact, we can achieve this by returning an incorrect character (e.g., X) and thus creating a separate expectation.
SCOPED_EXP ioCtx.read.expect_call()
and finally::returns('X');
This leads us to a crucial aspect of expectations:
By default, all available expectations are treated equally. Therefore, if multiple matches are possible for any incoming call, mimic++ will select any one of them.
In our case, since we only provide a single QuizItem,
we must ensure that the expectation for the invalid input is prioritized over the one for the valid input.
Otherwise, our Quiz will not prompt the user for another input,
leaving the expectations unfulfilled and resulting in a violation report.
This is where sequencing becomes essential, allowing users to create a reliable sequence of expectations.
Each expectation can only be matched when all of its predecessors are satisfied.
To facilitate this, mimic++ provides a mimicpp::SequenceT type, which we can use to attach our expectations.
mimicpp::SequenceT seq{};
SCOPED_EXP ioCtx.read.expect_call() // (1)
and expect::in_sequence(seq)
and finally::returns('X');
SCOPED_EXP ioCtx.read.expect_call() // (2)
and expect::in_sequence(seq)
and finally::returns(answer);
As you can see, we simply need to create a new sequence instance and attach the desired expectations using the expect::in_sequence policy.
This ensures that the first expectation is reliably matched before the second.
There is an even more convenient alternative that makes the sequence responsible for managing the lifecycle of the attached expectations. This approach simplifies the process by abstracting some of the details.
mimicpp::ScopedSequence seq{};
seq += ioCtx.read.expect_call() // (1)
and finally::returns('X');
seq += ioCtx.read.expect_call() // (2)
and finally::returns(answer);
This method is fully equivalent to the previous solution but offers a cleaner and more streamlined experience.
Times Policies
There is still yet another case we should take into consideration:
internally, Quiz::read_input utilizes a loop, so it continues to ask for input as long as it receives invalid input.
We can, of course, add another expectation into our sequence, but that wouldn’t scale well for more complex scenarios.
Therefore, mimic++ provides the expect::times policy family,
which allows you to specify exactly how often an expectation should match until it is satisfied.
This can be done by simply adding that policy to our builder chain:
seq += ioCtx.read.expect_call() // (1)
and expect::times(3) // 3 times is just arbitrarily chosen here.
and finally::returns('X');
Now, Quiz::read_input will receive the invalid input X exactly three times in a row until we supply the valid answer.
expect::times is a times-policy that can only be applied once for each expectation.
If no such policy is specified, mimic++ defaults to expect::once.
Such a times-policy may also specify a range (e.g. times::at_least(1)),
which denotes the minimum and maximum times an expectation must be matched.
When an expectation matches the
- minimum amount, it is called satisfied, and
- maximum amount, it is called saturated.
When we reconsider our previous expectations,
it does not make sense for Quiz to ask for user input before the actual question and the choices have been printed,
so they are also good candidates for inclusion in the sequence.
To sum it up, the final setup that we can use to replace the // (1) ... marker in our test case looks like this:
mimicpp::ScopedSequence ioSequence{};
IOContextMock ioCtx{};
ioSequence += ioCtx.write.expect_call("Q1");
ioSequence += ioCtx.write.expect_call(matches::range::eq(std::array{"A1", "A2", "A3", "A4"}));
// When we provide an invalid answer, we will get another chance.
// Let's force this three times in a row!
ioSequence += ioCtx.read.expect_call()
and expect::times(3)
and finally::returns('X');
// Let's now provide our actual answer.
ioSequence += ioCtx.read.expect_call()
and finally::returns(answer);
Conclusion
In this post, we explored the basics of how mocking can be done in mimic++. This included creating mocks and setting up expectations. Additionally, we touched on the topic of expectation policies and how sequences can be employed.
We tested our Quiz class, but we are not done yet!
As it stands, it is a very easy quiz since the correct answer is always A,
and the QuizItems always come in the same order.
Making it more interesting and creating the appropriate tests will be covered in a follow-up.
Until then, you might consider extending the Quiz App by adding these QuizItems into the item set.
But be careful!
The choices have been shuffled, so you need to determine the correct answer and place it in the 0th position.
| Question | A | B | C | D |
|---|---|---|---|---|
| What is the default amount an expectation must match? | Twice | Once | Never | 42 times |
| Is there a specific order in which expectations must be matched? | Ordering? | In declaration order. | In reverse declaration order. | No specific ordering required. |
| What does it mean when an expectation is satisfied? | Matched the minimum times. | Matched the maximum times. | Matched at least once. | It’s successfully linked with a mock. |
| What does it mean when an expectation is saturated? | Matched the minimum times. | Matched the maximum times. | Matched at least once. | It’s successfully linked with a mock. |
What is the role of the SCOPED_EXP macro? |
Creates a new mock object. | Starts a new expectation setup. | Stores an expectation and manages its lifetime. | Invokes a mock object. |