The refactorings I made in the previous episode, laid the groundwork for unit-testing. Now has come the time to talk about mocks. What are mocks, anyway?
From a component developer’s point of view, there are usually “foreign” components as well; that is, components that you are dependent on but you don’t want to unit-test. You don’t even want to #include or link-in such components when executing your unit tests. Often, such foreign components are developed by another person, department, or organization. In other cases, these components run only on a particular hardware. Sometimes, these components are not (fully) implemented yet when you start developing your component. Foreign components are outside the scope of the UUT (unit under test).
Mocks act as substitutes for foreign components. They adhere to the same interface as the foreign components, otherwise they wouldn’t be compatible. The cool thing about mocks is, that you, as a unit test developer, can specify how the mock should behave. So when you have a mocked Person class with a method “greet”, and the real implementation would normally respond with “Hello”, you could instruct your Person mock to return “hello”, “howdy”, “”, a nullptr, basically anything you want. This allows you to write unit tests that check that your UUT code behaves correctly, no matter what is returned by the greet method. Setting-up such behavior in the real foreign component would be extremely difficult, if not impossible. A mock is basically a puppet on a string — you pull the strings and it behaves accordingly.
Let’s do a concrete example using our temperature control app. We defined an “ISensor” interface and rewrote the whole app to only work against this interface. From this interface, we’ll derive a mock (I’ll use googlemock, one of the most popular, powerful, and reliable mocking frameworks for C++).
1 2 3 4 5 6 |
class MockSensor : public ISensor { public: MOCK_METHOD(int, getTemperature, (Status* status)); }; |
I suppose you can easily figure out what MOCK_METHOD does: It declares a method “getTemperature” that returns an “int” and takes “status” as a parameter, which is exactly the signature defined in “ISensor”. Now let’s toy around with this mock a little bit:
1 2 3 4 5 6 7 8 |
TEST(this_is_why_we_mock, mock_temperature_experiments) { MockSensor sensor; Sensor::Status status; EXPECT_CALL(sensor, getTemperature).WillOnce(Return(200)); EXPECT_EQ(200, sensor.getTemperature(&status)); } |
The interesting part is what happens in EXPECT_CALL; it basically means this:
- We expect, that during the execution of our test case, there will be exactly one call to “getTemperature” (“WillOnce”).
- We demand that the call to “getTemperature” yields a value of 200.
When we call the “getTemperature” method on the next line, “sensor” (our mock) will comply with our demand and return 200. That’s why the EXPECT_EQ statement will succeed. Let’s call “getTemperature” twice:
1 2 3 4 5 6 7 8 9 |
TEST(this_is_why_we_mock, mock_temperature_experiments) { MockSensor sensor; Sensor::Status status; EXPECT_CALL(sensor, getTemperature).WillOnce(Return(200)); EXPECT_EQ(200, sensor.getTemperature(&status)); EXPECT_EQ(200, sensor.getTemperature(&status)); } |
In this case, we’ll get
1 2 3 4 5 6 7 |
Mock function called more times than expected - returning default value. Function call: getTemperature(0x7ffcca3d1c04) Returns: 0 Expected: to be called once Actual: called twice - over-saturated and active |
If we wanted to set our expectations such that “getTemperature” is called multiple times, we could use “.WillRepeatedly” instead of “.WillOnce” and our test would thereafter pass.
What about the “status” parameter? Is there a way to demand that output parameters are set to a certain value as well? You bet!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
TEST(this_is_why_we_mock, mock_temperature_experiments) { MockSensor sensor; Sensor::Status status; EXPECT_CALL(sensor, getTemperature).WillOnce(DoAll( SetArgPointee<0>(ISensor::Status::bad), Return(200))); EXPECT_EQ(200, sensor.getTemperature(&status)); EXPECT_EQ(Sensor::Status::bad, status); EXPECT_CALL(sensor, getTemperature).WillOnce(DoAll( SetArgPointee<0>(ISensor::Status::good), Return(100))); EXPECT_EQ(100, sensor.getTemperature(&status)); EXPECT_EQ(Sensor::Status::good, status); } |
Here we have multiple demands on our mock (“DoAll”). The mock will set the value of the dereferenced first pointer argument to “bad” and the return value to 200. The same goes (with different values) for the second test case. Nice, isn’t it?
Since we now have a basic understanding of how mocking works, we can resume our task of unit-testing our temperature control app. Instead of using concrete sensor and heater objects, we’ll dependency-inject their mocked counterparts and set expectations and demands on our mocks. We then execute our UUT (the controller) and check if it behaves correctly (expect that the heater mock is turned on or off). For brevity’s sake, I’ll only show the “happy path” where the sensor status is never “bad”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
TEST(this_is_why_we_mock, happy_path) { MockSensor sensor; MockHeater heater; constexpr int targetTemp = 23; constexpr int hysteresis = 2; Controller controller(sensor, heater, targetTemp, hysteresis); // Temperature below target temperature, heater shall be turned on. EXPECT_CALL(sensor, getTemperature).WillOnce(Return(0)); EXPECT_CALL(heater, on); EXPECT_TRUE(controller.step()); // Temperature equals target temperature, heater shall be turned off. EXPECT_CALL(sensor, getTemperature).WillOnce(Return(targetTemp)); EXPECT_CALL(heater, off); EXPECT_TRUE(controller.step()); // Temperature below target temperature but within hysteresis. // Heater state shall not change. EXPECT_CALL(sensor, getTemperature).WillOnce(Return( targetTemp - hysteresis / 2)); EXPECT_CALL(heater, off).Times(0); EXPECT_CALL(heater, on).Times(0); EXPECT_TRUE(controller.step()); // Temperature below target temperature but still within hysteresis. // Heater state shall not change. EXPECT_CALL(sensor, getTemperature).WillOnce(Return( targetTemp - hysteresis)); EXPECT_CALL(heater, off).Times(0); EXPECT_CALL(heater, on).Times(0); EXPECT_TRUE(controller.step()); // Temperature below target temperature, outside hysteresis, // heater shall be turned on. EXPECT_CALL(sensor, getTemperature).WillOnce(Return( targetTemp - hysteresis - 1)); EXPECT_CALL(heater, on); EXPECT_TRUE(controller.step()); // Temperature below target temperature, whithin hysteresis. // Heater state shall not change. EXPECT_CALL(sensor, getTemperature).WillOnce(Return( targetTemp - hysteresis + 1)); EXPECT_CALL(heater, on).Times(0); EXPECT_CALL(heater, off).Times(0); EXPECT_TRUE(controller.step()); // Temperature above target temperature, heater shall be turned off. EXPECT_CALL(sensor, getTemperature).WillOnce(Return(targetTemp + 1)); EXPECT_CALL(heater, off); EXPECT_TRUE(controller.step()); } |
The remaining test cases can be found in this GitHub repository.
This concludes my mini series on mocking in C++. My aim was not to teach you all about mocking (or googlemock in particular), but rather wet your appetite. Another aim was to raise your level of awareness regarding “design for testability”, especially programming against interfaces instead of concrete classes. Your foreign components should always be derived from pure interfaces. Otherwise, you simply can’t dependency-inject mocks during unit testing — at least not in C++.
To be able to fully unit test our code — this is why we mock!