C++ tips for easier unit testing

A while back I read the Working Effectively with Legacy Code by Michael C. Feathers. The book focuses on strategies and approaches to get existing untested legacy code covered by unit tests.

The book also includes more than twenty dependency-breaking techniques that can be used for new code as well to make it easily testable in the first place. Here I have picked three of the techniques that I have personally found most useful.

  • Interfaces to abstract implementation
  • Extract and override call
  • Expose static method

Interfaces to abstract implementation

Using interfaces and abstractions is one of the main concepts in object-oriented design. It is also the most important technique to make components easily unit testable. Interfaces are used to break dependencies and decouple components from each other. This way they can be instantiated in the test harness and it is also possible to add sensing.

The example below show a class MessageHandler which has a member PersistentStorage for saving the received messages. The PersistentStorage stores the data in, for instance, local database or file.

class MessageHandler
{
public:
  // ...
private:
  PersistentStorage m_storage;
};
class PersistentStorage
{
public:
  // ...
  bool storeMessage(const Message& msg);

private:
  int m_fd;
};

Instantiating the MessageHandler creates the PersistentStorage instance, which in turn accesses the physical storage location. This dependency chain makes the MessageHandler hard to test without having side effects on the host system. It may also require additional dependencies such as external libraries.

This dependency can, however, easily be broken using an interface. The example below shows modified MessageHandler class that accepts the persistent storage as an interface.

class IPersistentStorage
{
public:
  virtual ~IPersistentStorage();
  virtual bool storeMessage(const Message& msg) = 0;
};
class MessageHandler
{
public:
  MessageHandler(std::shared_ptr<IPersistentStorage> storage);
  // ...
private:
  std::shared_ptr<IPersistentStorage> m_storage;
};

Now, by using a mock or fake implementation of IPersistentStorage, the class can be instantiated in the tests without actually trying to access any physical storage. The mock and fake classes can also be used to add extra sensing to the test cases (i.e. verify that the calls to storeMessage are correct).

  • Fake: object that actually has working implementation, but takes shortcut or simplification so that they cannot be used in production (e.g. in example above FakePersistentStorage could only store values in memory and not write them anywhere)
  • Mock: object with pre-programmed expectations and return values (no real implementation)

Using interfaces is a good practice when writing new code, but the Extract Interface refactoring shown above is also very useful when working with existing code. For C++, Google Mock provides a convenient library to implement mock classes.

Extract and override call

In some situation the component that is tested can be easily instantiated, but it has an effect that is not desirable in the testing environment (but is not large enough to be extracted to its own class and interface). The code below show an example of an undesirable effect on the testing environment.

bool MessageHandle::process(const Message& msg)
{
  switch(msg.type)
  {
    //...
  MSG_REBOOT:
    system("reboot");
    break;
  }
  //...
}

If the reboot message is unit tested, the code would try to reboot the testing machine which is certainly unwanted. Below the functionality is extracted to separate member function.

class MessageHandler
{
  //...
private:
  virtual void reboot();
};
bool MessageHandler::process(const Message& msg)
{
  switch(msg.type)
  {
    //...
  MSG_REBOOT:
    reboot();
    break;
  }
  //...
}

void MessageHandler::reboot()
{
  system("reboot");
}

The function is declared virtual so that it can be overridden for testing.

class TestingMessageHandler : public MessageHandler
{
public:
  bool reboot_called; // simple sensing
  //...
private:
  virtual void reboot() override {reboot_called=true;}
};

In the unit tests, TestingMessageHandler is used instead of MessageHandler. The only difference between the classes is, that when the reboot function is called the testing class only sets the sensing variable whereas the production version actually runs the reboot-command.

Expose static method

Exposing a static method allows to write unit tests directly for internal class member functions. This is useful when the internal logic of the class cannot be thoroughly tested using the public interface. In the example below, testing the isNewerVersion thoroughly is difficult through the update interface because the first parameter is taken from constant.

bool Updater::update(const FirmwareInfo& fw)
{
  if(!isNewerVersion(APP_VERSION, fw.version)
  {
    return false;
  }
  //...
}

Easiest way is to make the function static and part of the class’ public interface. Then unit tests can be written directly for it without instantiating the actual class. However, if the function is such that it should not be available in the public interface, it is also possible to expose it only for testing as shown below.

class Updater
{
  //...
protected:
  static bool isNewerVersion(const std::string& curver,
                             const std::string& newver);
};

 

class TestingUpdater : public Updater
{
public:
  using Updater::isNewerVersion;
  //...
};

TEST(Updater, Example)
{
  // Static function publicly available for testing but
  // not in production class
  ASSERT_FALSE(TestingUpdater::isNewerVersion("1.0", "0.1"));
}

Now the function is accessible in test cases, but the protected visibility makes sure that it is not called directly in the production code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s