Integrating Mocking With C++ Default Parameters
When we put a piece of code into a unit test, sometimes we need to hammer it into a shape that fits into a test harness. A typical example is for cutting dependencies: the function we’d like to test depends on UI, a database, or just something really intricate that our test binary can’t link against.
Some of those refactoring operations on tested function are beneficial: its dependencies become fewer and clearer, and the resulting code has less coupling.
But sometimes, all this hammering has the effect of leaving the tested function in a pretty bad shape. For example mocking can affect its interface when we use it to replace a inner part of the function.
This article is part of the series on default parameters in C++:
- Default parameters in C++: the facts (including the secret ones)
- Should I overload or use default parameters?
- Default Parameters With Default Template Type Parameters
Defaulted
: a helper to work around default parameters constraints- Implementing Default Parameters That Depend on Other Parameters in C++
- How default parameters can help integrate mocks
An example of mocking
For instance, let’s consider a function f
that happens to call a logging function to output some of its results:
int f(int x, int y) { // doing calculations... log(intermediaryResult); // calculating some more... return result; }
And we won’t compile the logging code into the test binary. In fact, we don’t even need f
to log anything at all when it runs in its unit test.
EDIT: as several readers pointed out, some loggers are implemented with a global access and can be deactivated, without the need of a mock. Here the example aims at illustrating any piece of code that you don’t want to include in your test binary. So log
could be replaced with compute
or convert
or doSomething
, as long as it represents a function of which we don’t want the code in the test binary, and that we replace by a mock.
There are several ways to deal with this sort of situation, and one of them, known as the ‘Extract Interface‘ refactoring, consists in mocking the logging functionality with a simpler implementation (here, that doesn’t do anything) and passing this mock to f
. (You can find many other ways to test such a function in Michael Feathers’ Working Effectively With Legacy Code).
The idea of mocking goes along those lines: we start by creating a interface with the functionalities we want to mock:
class ILogger { public: virtual void log(int value) const = 0; };
Then we create a class that implements this interface, to be used in the test, and that does not depend on the logging function:
class LoggerMock : public ILogger { public: void log(int value) const override { /* do nothing */ } };
And another class that actually performs the call to the log
function, to be used in production code:
class Logger : public ILogger { public: void log(int value) const override { ::log(value); } };
Then f
needs to change in order to accommodate this new interface:
int f(int x, int y, const ILogger& logger) { // doing calculations... logger.log(intermediaryResult); // calculating some more... return result; }
The production code calls f
this way:
f(15, 42, Logger());
and the test code calls it that way:
f(15, 42, LoggerMock());
In my opinion, f
got damaged in the process. In particular at the level of its interface:
int f(int x, int y, const ILogger& logger);
The logger was supposed to be an implementation detail of f
and it has now floated up to its interface. The concrete problems this causes are:
- whenever we read a call to
f
we see a logger mentioned, which is one more thing we need to figure out when reading a piece of code. - when a programmer wants to use
f
and looks at its interface, this interface demands to be passed a logger. This inevitably prompts the question: “What argument should I pass? I thoughtf
was a numerical function, what am I supposed to pass as a ‘logger’??” And then the programmer has to dig more, possibly ask the function’s maintainers. Oh it’s used for testing. Ah, I see. So what exactly should I pass here? Would you have a snippet that I could copy-paste into my code?
This is a hard price to pay for putting a function into a unit test. Couldn’t we do it differently?
Hiding the mock in production code
Just to be clear, I don’t have anything against the idea of mocking. It’s a practical way to put existing code into automatic testing, and automatic testing has immense value. But I don’t feel very well equipped with specific techniques in C++ to achieve mocking, and testing in general, without damaging the production code in some cases.
I’d like to point to a way to use default parameters to ease mocking in C++. I’m not saying it’s perfect, far from that. By showing it here, I’m hoping this will be interesting enough to you so that we can start exchanging on the subject as a group, and find together how to use the power of C++ to make testable code expressive.
There are at least two things we could do to limit the impact on f
: setting the mock as a default parameter, and using naming to be very explicit about its role.
Default mock parameter
Let’s set the mock parameter as a default parameter, defaulting to the production implementation:
int f(int x, int y, const ILogger& logger = Logger());
To achieve this we need the function to take the mock either by reference to const, or by value.
In this case the production code doesn’t have to worry about passing it a logger value any more:
f(15, 42);
The default way of acting of f
is the natural one: its calls to the log
function perform logging. No need for the call site to be explicit about that.
On the side of the test harness however, we want to do something specific: prevent the logging calls to reach the log
function. It makes sense to show at call site that something has changed:
f(15, 42, LoggerMock());
A naming convention
To clear up the doubts one could have about the last parameter when looking at the interface, we can use a specific name to designate this pattern. Taking inspiration in Working Effectively With Legacy Code, I like to use Michael Feathers’ notion of “seam”. It represents a point in code where we can plug several implementations. A bit like a seam is a place of junction between two pieces of fabric, where you can operate to change one of them without damage.
So our interface could be called LoggerSeam
instead of ILogger
:
int f(int x, int y, const LoggerSeam& logger = Logger());
This way, the word “Seam” in the interface conveys the message “Don’t worry, we just need this for testing purposes”, and the default parameter says “We got this handled, now carry on with your normal usage of f
“.
Going further
This was a very simple example of mocking, but there are other issues worth investigating. What if there were several things to mock in the function, and not just the logging? Should we have several seams and as many parameters, or a big one that contains everything the function needs to mock?
And what if the mock contained data, and not just behaviour? We couldn’t construct it in a default parameter. But isn’t mocking just about behaviour anyway?
Another point to note is that with the above implementation, if the function is declared in a header file, the default Logger has to be defined next to the function declaration, because the default parameter in the prototype calls its constructor.
In short: how do YOU think we can make testable code more expressive?
You may also like
Don't want to miss out ? Follow:   Share this post!