-
Notifications
You must be signed in to change notification settings - Fork 0
Effective Use of Test Doubles
The Test Architecture Patterns page introduced the vocabulary: dummy, stub, fake, spy, mock. This chapter focuses on practical usage: when to reach for each type, how to write them well, and the common mistakes that make test doubles a maintenance burden instead of an asset.
The most common mistake is reaching for a mock when a fake would do. Use the simplest type that makes the test clear.
Does the test need to control what the dependency returns?
Yes → Stub or Fake
No → Dummy
Does the test need to verify that a method was called?
Yes → Spy or Mock
No → Stub or Fake
Is the verification about exact call count or argument values?
Yes → Mock (GMock)
No → Spy
A useful rule: if the test only cares about the output of the class under test, use a fake. If the test cares about how the class interacted with a collaborator, use a mock.
A dummy satisfies a constructor or method parameter that the test does not exercise. It is never called.
// ILogger is required by DeviceMonitor but not relevant to this test
class NullLogger : public ILogger {
public:
void log(const std::string&) override {} // does nothing
};
TEST(DeviceMonitorTest, IsOverdueReturnsTrueWhenUptimeExceedsThreshold)
{
NullLogger logger; // dummy: required, but never called in this test
FakeClock clock;
clock.setUptime(100.0);
DeviceMonitor monitor(logger, clock);
EXPECT_TRUE(monitor.isOverdue(50.0));
}Keep dummies in a shared header so they are reused across test files rather than duplicated.
A stub returns a controlled value. It has no memory of past calls and no assertions of its own.
// Stub: always returns the same value set before the test
class StubClock : public IClock {
public:
void setUptime(double seconds) { uptime_ = seconds; }
double uptimeSeconds() override { return uptime_; }
private:
double uptime_ = 0.0;
};Stubs are the right choice when you need to put the class under test into a specific state by controlling what its dependency returns.
TEST(DeviceMonitorTest, ReportStatusLogsCurrentUptime)
{
FakeLogger logger;
StubClock clock;
clock.setUptime(42.0);
DeviceMonitor monitor(logger, clock);
monitor.reportStatus();
EXPECT_THAT(logger.messages[0], HasSubstr("42"));
}A fake is a lightweight, working implementation. It does not call real hardware or I/O, but it behaves correctly according to the interface contract.
// Fake in-memory key-value store: behaves like a real store, no filesystem
class FakeConfigStore : public IConfigStore {
public:
void set(const std::string& key, const std::string& value) override {
store_[key] = value;
}
std::string get(const std::string& key) const override {
auto it = store_.find(key);
return it != store_.end() ? it->second : "";
}
bool contains(const std::string& key) const override {
return store_.count(key) > 0;
}
private:
std::map<std::string, std::string> store_;
};Fakes are particularly valuable when the class under test uses the dependency in complex ways across multiple calls, and a simple stub would require too much setup per test.
TEST(ConfigManagerTest, ValueWrittenByOneMethodIsReadableByAnother)
{
FakeConfigStore store; // fake: stateful, no filesystem
ConfigManager manager(store);
manager.saveCalibration("sensor_offset", 1.5f);
float offset = manager.loadCalibration("sensor_offset");
EXPECT_FLOAT_EQ(offset, 1.5f);
}A spy is a fake that also records how it was called. It lets you assert on side-effects without using GMock.
class SpyLogger : public ILogger {
public:
void log(const std::string& message) override {
messages_.push_back(message);
}
int callCount() const { return static_cast<int>(messages_.size()); }
const std::string& lastMessage() const { return messages_.back(); }
const std::vector<std::string>& allMessages() const { return messages_; }
private:
std::vector<std::string> messages_;
};Use a spy when you want to assert that a side-effect occurred and what it contained, but you do not need GMock's strict call-count or argument matching.
TEST(DeviceMonitorTest, ReportStatusLogsExactlyOnce)
{
SpyLogger spy;
FakeClock clock;
DeviceMonitor monitor(spy, clock);
monitor.reportStatus();
EXPECT_EQ(spy.callCount(), 1);
}
TEST(DeviceMonitorTest, ReportStatusMessageContainsUptime)
{
SpyLogger spy;
StubClock clock;
clock.setUptime(77.0);
DeviceMonitor monitor(spy, clock);
monitor.reportStatus();
EXPECT_THAT(spy.lastMessage(), HasSubstr("77"));
}Spies are simpler than GMock mocks and produce clearer failure messages because the assertion is written in the test, not buried in an expectation macro.
Use GMock when you need strict control over interactions: exact call count, specific argument values, or call ordering. GMock reports failures at the point of violation, not at the end of the test.
class MockLogger : public ILogger {
public:
MOCK_METHOD(void, log, (const std::string& message), (override));
};TEST(DeviceMonitorTest, ReportStatusLogsUptimeValue)
{
MockLogger mock;
StubClock clock;
clock.setUptime(5.0);
DeviceMonitor monitor(mock, clock);
EXPECT_CALL(mock, log(HasSubstr("5"))).Times(1);
monitor.reportStatus();
}TEST(DeviceMonitorTest, LoggerNotCalledWhenReportStatusIsNotInvoked)
{
MockLogger mock;
FakeClock clock;
DeviceMonitor monitor(mock, clock);
EXPECT_CALL(mock, log(testing::_)).Times(0);
// reportStatus() deliberately not called
}TEST(DeviceMonitorTest, ShutdownLogsBeforeAndAfterHardwareStop)
{
MockLogger mock;
FakeClock clock;
DeviceMonitor monitor(mock, clock);
testing::InSequence seq;
EXPECT_CALL(mock, log(HasSubstr("stopping"))).Times(1);
EXPECT_CALL(mock, log(HasSubstr("stopped"))).Times(1);
monitor.shutdown();
}GMock EXPECT_CALL couples the test to the internal call sequence of the class under test. If the implementation changes the order or frequency of calls without changing observable behaviour, the test will break even though nothing is wrong. Prefer spies or fakes when the interaction details are not part of the contract being tested.
If a fake is used in only one test file, define it in that file. If it is shared across multiple test files, move it to a dedicated header in the test directory.
workshop/
├── 02_unit_tests/
│ ├── test_doubles.h // shared FakeLogger, FakeClock, NullLogger
│ ├── good_tests.cpp
│ └── ...
A test double with bugs produces tests that pass for the wrong reason. Keep fakes simple, test them if they have non-trivial logic, and review them when the interface they implement changes.
A fake that carries state between tests causes order-dependent failures. Always create fresh test doubles in the fixture setup, never as shared static instances.
// ✅ Fresh doubles per test via fixture
class DeviceMonitorTest : public ::testing::Test {
protected:
SpyLogger logger; // fresh for each TEST_F
StubClock clock;
DeviceMonitor monitor{logger, clock};
};
// ❌ Shared static fake: state bleeds between tests
static SpyLogger sharedLogger;Only set expectations on interactions that are part of the behaviour being tested. Unrelated EXPECT_CALL entries make tests fragile and hard to read.
// ❌ Over-specified: the test cares about threshold logic, not logging details
EXPECT_CALL(mock, log(HasSubstr("Uptime"))).Times(1);
EXPECT_CALL(mock, log(HasSubstr("check"))).Times(1);
monitor.reportStatus();
EXPECT_TRUE(monitor.isOverdue(50.0)); // the actual thing being tested
// ✅ Focus: only assert what this test is about
monitor.reportStatus();
EXPECT_TRUE(monitor.isOverdue(50.0));| You need to... | Use |
|---|---|
| Satisfy a required parameter that is irrelevant to the test | Dummy |
| Control what the dependency returns | Stub |
| Replace a stateful collaborator (store, queue, cache) | Fake |
| Assert that a side-effect occurred | Spy |
| Assert exact call count, arguments, or ordering | Mock (GMock) |
See also: Test Architecture Patterns, Module 02: Unit Tests, Module 05: Dependency Injection
- Home
- Test Architecture Patterns
- Designing for Testability
- Effective Use of Test Doubles
- Refactoring Toward Testability
- Deterministic Testing
- Testing Processes
- Test Plan Template
- Development Ticket Template
- Black-Box Testing from Doxygen
Module 01 — Code Structure
Module 02 — Unit Tests
Module 03 — Hardware Dependencies
Module 04 — Qt Signals & Slots
Module 05 — Dependency Injection