Unit tests: mocks, fakes and spies
Mocks, fakes and spies are techniques for isolating a subset of the component graph for unit tests
We’re going to look at a small code example, then we’re going to look at a unit test for that code using mocks, fakes and spies. From this concrete example we’re going to explain the terminology and how it generalizes.
Code
Here is some fictional code that we would like testing:
transfer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class transfer
{
public:
transfer(
contacts_db_interface & src,
contacts_db_interface & dst);
public:
void read(int id);
void save();
private:
contacts_db_interface & src_;
contacts_db_interface & dst_;
std::vector<contact> cache_;
};
transfer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
transfer::transfer(
contacts_db_interface & src,
contacts_db_interface & dst):
src_ { src },
dst_ { dst }
{}
void transfer::read(int id) {
auto item = src_.find_by_id(id);
if (item.has_value()) {
cache_.push_back(*item);
}
}
void transfer::save() {
for (const auto & item : cache_) {
dst_.insert(item);
}
cache_.clear();
}
What we see is that we have this transfer class that can be used to read
contacts by id from a source contacts_db and save the accumulated reads
to a destination contacts_db. Both contacts_dbs are accessed via the
interface contacts_db_interface that looks like this:
contacts_db_interface.h
1
2
3
4
5
6
7
8
9
10
11
12
struct contact {
int id;
std::string name;
std::string email;
};
struct contacts_db_interface {
virtual std::optional<contact> find_by_id(int id) = 0;
virtual void insert(const contact & item) = 0;
virtual ~contacts_db_interface() = default;
};
Graph
Given the code above we can picture a subset of the program graph of components:
- there are some users of the
transaction - the
transactionpresumably uses twocontacts_dbvia the interface and also uses thecontactand thestd::vectorcache_. - the
contacts_dbeach use some component for database access and alsocontact contactusesstd::stringstd::stringandstd::vectorboth allocate memory on the heap
In a well written program the graph is a DAG (directed acyclic graph):
- the edges between entities are directed from the one that uses to the one that is used
- the graph is acyclic: there are no directed cycles, there is a clear hierarchy of who is using what
Unit test
We can now test the transaction class, using a unit test framework, with the
view of using mocks and fakes to reduce the area of the graph that is tested.
transaction_test.h
We first create a mock type to replace the source contacts_db. For each
method that the mock implements we use in this case the macro MOCK_METHOD.
The macro declares the target function, and a member variable that is used to
store a state machine which drives the implementation of that function.
1
2
3
4
5
6
struct contacts_db_mock :
public contacts_db_interface
{
MOCK_METHOD(std::optional<contact>, find_by_id, (int), (override));
MOCK_METHOD(void, insert, (const contact &), (override));
};
We then create a fake type to replace the destination contacts_db. We’ve
chosen a mock for the source and a fake for the destination just for the sake
of exercise, in practice we would use the same approach for both.
Instead of a real database, we store the data in memory in the member variable
data_ and we implement the methods as manipulation of the member variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct contacts_db_fake :
public contacts_db_interface
{
std::vector<contact> data_;
std::optional<contact> find_by_id(int id) override {
auto it = std::find_if(data_.cbegin(), data.cend(),
[=id](const contact & c) {
return c.id = id;
});
if (it == data.cend()) return {};
return *it;
}
void insert(const contact & item) override {
data_.push_back(item);
}
};
I’m omitting the details on how to implement memory_spy, but imagine an
object which intercepts heap memory allocations and deallocations for the
purpose of keeping tallies e.g. ensure that they match and there are no
accidental memory leaks.
We are now ready to do the actual test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TEST(transaction_test, simple)
{
// instantiate spy, mock and fake
memory_spy mem;
StrictMock<contacts_db_mock> src;
contacts_db_fake dst;
// setup mock expectations
EXPECT_CALL(src, find_by_id(42))
.WillOnce(Return(contact{ 42, "Bob", "bob@example.com"));
// trigger test
transaction t{ src, dst };
t.read(42);
t.save();
// check fake state
const std::vector<contact> expected_data = {
{42, "Bob", "bob@example.com"},
};
EXPECT_EQ(expected_data, dst.data);
}
Terminology and observations
Notice that although called “unit test”, the test covers multiple entities, a subset of the components graph of the program, not just the entity under test.
In our case the entity under test is the transaction, but the test also
covers others: contact, std::string and std::vector.
The entities can be classes/objects, but also functions.
When the entity under test has a small or empty graph underneath it, it’s easy to test, especially for regular functions that perform some calculation as we’ve seen in the previous article.
But when the entity under test has a large graph underneath it, we need ways to cut it, to reduce it to a smaller one, so we need some replacements for the real entities in the real components graph. The generic name for replacements for test purposes is doubles (like a person substituting an actor on a film set). Mocks and fakes are such doubles.
Fakes are doubles that try to simulate the real implementation. The problem is that they require quite a lot of non-repetitive code for that simulation.
Mocks are doubles that do not even try to simulate the real implementation.
Instead they have support from the unit test framework to generate state
machines that provide canned responses. That’s what the combination of
MOCK_METHOD and EXPECT_CALL does in our case.
The difference between fakes and mocks usually reflects in different styles of
verification. Mocks encourage behaviour verification where we check how the
entity under test performs the task. In our case we set up the expectation that
find_by_id will be called once with value 42 for the id argument, and by
using the StrictMock that no other calls will be made. Another style is
state verification where we check the end result (regardless of how we got
there). In our case this is shown in the use of the fake, where we check at the
end that the data_ has the expected content.
I’ve also alluded to the spy technique (the memory_spy in our case),
which is a wrapper around the real entity with the purpose of intercepting the
calls in order to do some record keeping. This allows some introspection for
test purposes, but does not decrease the size of the component graph under
test.