Everytime I watch one of those “This is why we ride” videos, I immediately want to jump on my bike and head for some twisty mountain roads. What’s funny is that after watching these videos, even people who don’t have a driver’s license feel a strong urge to jump on a bike.
I wrote this post in a similar spirit, hoping to convince C++ developers that unit testing with mocks is both, necessary and fun. My main focus is presenting a case for using mocks, since I regularly meet even experienced coders who are not familiar with mocks. If you’re not familiar with mocks, it’s a clear sign that you have never really unit-tested your code. It’s that simple! You might have unit-tested some free-standing library or algorithm that you wrote, but when an application consists of multiple interacting components implemented by different people, there’s no way for you to test your component in isolation without linking-in the components you depend on. If you do so, you’re not doing unit testing. You’re rather doing integration testing. And what do you do if the components you depend on are still under development? Do you postpone your work and wait until they are fully done? What if some components are hardware-dependent and only available on the target platform? Do you leave the comfort of your host platform and execute your tests only on the target platform? No! What you do is sprinkle some “design for testability” over your code and test against mocks. Suck it up!
Let’s do an example, a very basic temperature control application. It consists of a heat sensor, a heater, and the controller. The controller is a simplistic on-off controller which reads the current temperature from the sensor and compares it against a target temperature. If the temperature is below the target temperature, the controller turns the heater on, if the temperature is above the target temperature, it turns it off. To make it a bit more interesting, I added another requirement.
To avoid frequent on/off toggling of the heater around the target temperature point, the controller supports so-called hysteresis, a temperature range below the target temperature in which the controller doesn’t turn on the heater even though there is a deviation from the target temperature. Here’s an example: if the target temperature is set to 23 degrees and there’s a hysteresis of 2 degrees, the controller will not turn on the heat if the current temperature is 22 degrees. Not even if the current temperature is 21 degrees. It will, however, turn on the heater at 20 degrees (or less) as this is outside the range of the hysteresis.
But wait, there’s another point to mention. The temperature sensor provides a status, which tells if the temperature reading can be relied upon. If the sensor status is “bad”, the measurement must be ignored. If the status is “bad” for two times in a row, it means that the sensor is broken and the controller application shall (for the sake of safety) turn the heater off and exit.
If you like, you can pause here and think about how to implement and test this application.
Done? Here’s my naive implementation:
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 |
class Sensor { public: enum class Status { good, bad }; int getTemperature(Status* status) { /* access hardware */ } }; class Heater { public: void on() { /* access hardware */ } void off() { /* access hardware */ } }; class Controller { public: Controller(int target, int hysteresis) : m_target(target) , m_hysteresis(hysteresis) { } void run() { Sensor::Status currentStatus{ Sensor::Status::good }; Sensor::Status previousStatus { Sensor::Status::good }; for (;;) { int temperature = m_sensor.getTemperature(¤tStatus); if (currentStatus == Sensor::Status::good) { if (temperature >= m_target) { m_heater.off(); } else if (temperature < m_target - m_hysteresis) { m_heater.on(); } else { // Leave heater as is. } } else { if (previousStatus == Sensor::Status::bad) { // Second time in a row bad sensor data. // Give up. m_heater.off(); break; } } previousStatus = currentStatus; } } private: int m_target; int m_hysteresis; Sensor m_sensor; Heater m_heater; } |
Does this code implement the requirements correctly? I don’t know for sure, but I assume it does. Do you agree?
Correct or buggy, it’s hard to agree. But we can easily agree on one thing: this code is utterly untestable. In the next part, I’ll refactor it such that it becomes one hundred percent testable, which allows us to move from correctness assumptions to knowledge. Stay tuned!