New C++11 features can be used to implement mock objects for unit tests. Michael Rüegg shows us how he does this in Mockator.
In our last article ‘Refactoring Towards Seams in C++’ (appeared in issue 108), we described how we can break dependencies in legacy code by applying seams with the help of our engineered refactorings. Once we have managed to apply seams, our code is not relying on fixed dependencies anymore, but instead asks for collaborators through dependency injection. Not only has our design greatly improved, but we are now also able to write unit tests for our code.
Sometimes it is impractical or impossible to exercise our code with real objects. If a real object supplies non-deterministic results, is slow or contains states that are difficult to create, then we might want to use mock objects to test our objects in isolation. In this article we present how we can mock objects by creating a small but useful mock object library that makes use of the new language features of C++11.
What is a seam? | |
|
How to mock objects
To start with an example, consider the system under test (SUT)
Trader
shown in Listing 1.
Trader
has a fixed relationship to
Nasdaq
which makes it hard to test in isolation because its operations require network calls which we do want to avoid when we run our unit tests.
Nasdaq
is therefore a good candidate to be replaced with a mock object.
#include "nasdaq.h" struct Trader { void stopLoss(std::string symbol, unsigned int amount, Price lowerLimit) { Share share = exchange.lookupBy(symbol); // causes network call Price currPrice = share.currentPrice(); if (currPrice < lowerLimit) exchange.sell(share, amount); // dito } } private: Nasdaq exchange; }; |
Listing 1 |
One of the many possibilities in C++ to inject dependencies from outside is to extract a template parameter and to inject the dependency at compile time making use of parametric polymorphism. We therefore call this seam type compile seam . After applying the refactoring extract template parameter , the code results as shown in Listing 2.
#include "nasdaq.h" template<typename STOCKEXCHANGE=Nasdaq> struct TraderT { void stopLoss(std::string symbol, unsigned int amount, Price lowerLimit) { // as before } private: STOCKEXCHANGE exchange; }; typedef TraderT<> Trader; |
Listing 2 |
This code has a seam because we now have an enabling point: the place where the template class
TraderT
is instantiated. Note that we create a
typedef
which instantiates the template with the concrete type that has been used before applying the refactoring (here through the use of a default template parameter). This has the advantage that we do not break existing code that still wants to use
Nasdaq
.
We now give a complete example of how we think mocking objects should be done in C++ and explain the internals of our approach in the subsequent sections of this article. We apply the classic unit test work flow proposed by the xUnit pattern [ Meszaros07 ] in Listing 3: setup, exercise, verify and teardown (whereas the latter is not necessary here).
#include "mockobjects.h" #include <cassert> void test_sell_shares_when_current_price_below_stop_loss_limit() { // setup static std::vector<calls> allCalls{1}; struct MockExchange { MockExchange() : mockid{reserveNextCallId(allCalls)} { allCalls[mockid].pushback (call{"MockExchange()"}); } Share lookupBy(std::string symbol) { allCalls[mockid].pushback (call{"lookupBy(std::string)", symbol}); return {Share{symbol, Price{29}}; } void sell(Share share, unsigned int amount) { allCalls[mockid].pushback (call{"sell(Share, unsigned int)", share, amount}); } const size_t mockid; }; // exercise TraderT<MockExchange> trader; trader.stopLoss("FB", 1000000, Price{30}); // verify calls expected = { {"MockExchange()"}, {"lookupBy(std::string)", "FB"}, {"sell(Share, unsigned int)", "FB", 1000000} }; assert(expected == allCalls[1]); } |
Listing 3 |
We use the vector
allCalls
to register all function calls the SUT makes on the injected local class
MockExchange
. It needs to be defined static because of the shortcomings local classes still have with C++11. We create the vector initially with a size of one. Index 0 is reserved for calls of static member functions on the mock object. Note that every mock object has a mock ID which is used to access the calls made by the SUT on a specific instance of the mock object class. In the registrations of the function calls, we use this to store the call for the corresponding mock object instance. Also note the usage of C++11’s new initialiser lists for specifying our expectations. At the end of our example, we assert the calls made with the index 1 (we only have one instance of
MockExchange
) with our expectations.
We think it is worthwhile to have the code for the mock object in the unit test without hiding it behind DSL’s built up of macros as other mock object libraries do. This yields more transparency and exploits the full power of the host language when the library does not provide a desired feature. Furthermore, we circumvent the numerous problems that come with the application of macros.
In the classic mock object approach the unit test does not exercise any assertions. This is entirely handled by the mock object which – when called during SUT execution – compares the actual arguments received with the expected arguments using equality assertions and fails the test if they do not match. We have decided against this common approach and exercise the assertions in the unit test itself because we want to be independent of the underlying unit testing framework. We therefore do not assert for equality in the mock object member functions, but instead compare the string traces in the unit test. Also note that our comparisons are order-sensitive and therefore we use strict mock objects [ Meszaros07 ].
Local classes: 2nd class citizens in the C++ world | |
|
How to record function calls
An important part of a mock object implementation is the recognition of the function calls the SUT makes on the mock object while a unit test runs. Beside the sequence and number of calls, we are also often interested in their argument values. We therefore have to store these facts to be able to later compare the calls with the users expectations.
We use an abstraction named
call
for this purpose which represents a call of a function. Its basic functionality is shown in Listing 4.
// mockobjects.h #include <sstream> #include <vector> struct call { template<typename ...Param> call(std::string const& funSig, Param const& ...params) { record(funSig, params ...); } template<typename Head, typename ...Tail> void record(Head const& head, Tail const& ...tail) { std::ostringstream oss; toStream(oss, head); if (sizeof...(tail)) { oss « ","; } trace.append(oss.str()); record(tail ...); } void record() { } std::string trace; }; typedef std::vector<call> calls; |
Listing 4 |
A function call consists of the signature of the function and its argument values. Because we have to allow arguments of any type, we use a template parameter for the arguments in the constructor of
call
. Due to the fact that we do not want to restrict the number of arguments, we use a variadic template parameter pack.
The constructor of
call
uses the variadic template member function
record
to recursively process the arguments of the function call.
record(Head const&, Tail const&)
is used as the recursion step whereas
record()
handles the basic case of the recursion. Note the use of template parameter unpacking in the
sizeof
call to separate the argument values with commas and for the recursive call in record.
call
uses a
std::string
object to store the function signature and the argument values. This is used to remember the values of any possible argument type and to give the user as much information as possible when a comparison fails. Also note the
typedef calls
which we use to store the calls on an instance of a mock object class.
What are variadic templates? | |
|
Requirements on function parameter types
To store the argument values in a string, we expect that types used for the function arguments implement a corresponding
operator(ostream&, Type)
. To prevent cryptic compiler errors if this is not the case, we use some template meta programming tricks taken from the Boost exception library. The interested reader might want to have a look at the file
is_output_streamable.hpp
in a recent Boost library version to see how this works. This is done in the function
toStream
which delegates the work of using the stream output operator in case it is defined and otherwise writing a message into the stream to inform the user about the missing operator.
template<typename T> std::ostream& toStream(std::ostream& os, T const& t) { selectbuiltinshiftif<T, isoutputstreamable<T>::value> out(os); return out(t); }
Specifying expectations with initialiser lists
When unit testing our objects, we want to compare a list of function calls against our expectations. We use initialiser lists for specifying expectations the SUT has to fulfil. Note that C++ always allowed initialisation of plain old data (POD) types and arrays with initialiser lists, i.e., to give a list of arguments in curly brackets. But it was not possible in the old standard to use initialiser lists with regular (non-POD) classes. This has changed with C++11 where we are now able to instantiate regular classes with initialiser lists. This can be seen here with our
calls
vector.
calls expected = { {"foo(int i)", 42}, {"bar(char c)", 'x'}, {"foo(std::string s, double d)", "mockator", 3.1415} };
In order to make comparisons work between the actual executed calls of the SUT on the mock object and our expectations, we have to provide an equality operator for
call
.
operator==
just delegates the work to the equality operator of
std::string
to compare the traced function call. To allow unit testing frameworks to print a string representation of the object under consideration if a comparison fails, we also provide a stream operator for
call
.
bool operator==(call const& lhs, call const& rhs) { return lhs.trace == rhs.trace; } std::ostream& operator«(std::ostream& os, call const& c) { return os « c.trace; }
Another important thing to explain is the function
reserveNextCallId
applied in Listing 3. This function is used to initialise the ID of the mock object and to add another call vector to the
allCalls
vector which collects all calls made on all instances of the mock object class. Its implementation is:
size_t reserveNextCallId (std::vector<calls> &allCalls) { size_t counter = allCalls.size(); allCalls.pushback(calls{}); return counter; }
Reference implementation
Based on the mock object library discussed in this article, we have implemented Mockator. Mockator is a plug-in for the Eclipse C/C++ Development Tooling (CDT) platform including a header-only C++ based mock object library. The library also supports order-independent comparisons, the use of regular expressions in the expectations, nice string representations for easier comparisons of STL containers when used as function arguments and C++03 beside C++11.
Because common mock object libraries often lack good IDE support, we implemented a plug-in for Eclipse CDT that – beside its support for seams presented in the foregoing article –recognises missing member functions the SUT calls on the mock object and is able to generate them including the presented call registrations. It is able to generate code for both C++ standards and not only supports the common mock objects based on inheritance, but also ones based on parametric polymorphism.
We recognised that it is often beneficial to just mock a single function instead of extracting an interface or a template parameter for classes. Therefore, we also implemented mocking of functions. Additionally, we provide various convenience functions to make working with mock objects easier like moving them to a namespace (useful if the unit test gets too big because of the mock object code and to share mock objects between unit tests), converting fake to mock objects, toggling the call recording on a member function level and recognising inconsistent expectations. The interested reader can download Mockator and give it a try. It is available as an alpha version under [ Rüegg12 ].
References
[Feathers04] Working Effectively With Legacy Code , Michael C. Feathers 2004
[ISO/IEC11] Working Draft, Standard for Programming Language C++, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf , February 2011
[Meszaros07] Gerard Meszaros, Unit Test Patterns: Refactoring Test Code , Addison-Wesley 2007
[Rüegg12] Michael Rüegg, ‘Mockator’, available from http://www.mockator.com , 2012