Even modern versions of C++ makes it trivial to implement exploitable bugs. However, as the test frameworks, compilers, C++ and the standard library have matured, there are no longer any reasons for not making tests to find those bugs (and prevent any future regression bugs while you're at it). If your code is hard to test, it is most likely a sign of bad code structure and architecture, and you will most likely benefit from adapting to something that is more testable.

In this article I will point out how to make your code testable, and how to actually implement unit tests (using GoogleTest), how to make integration tests using stubs or mocks (using gMock, now part of GoogleTest), how to check and visualise code coverage (using lcov) and making it a part of your build pipeline (using GitHub Actions).

 

Google Test

For only testing small pieces of code, Google Test is a very handy unit test framework. Installing it is just a matter of

`sudo apt-get install libgtest-dev`

using it is just

#include "gtest/gtest.h"

and linking it is just adding to your Makefile LIBSFLAGS

-lgtest

 

After enabling Google Test you will get access to a few simple macros (TEST, and quite a few EXPECT_*).
If you have a time-class with a constructor that takes a unix time_t as parameter, you could make test named "EpochTest" belonging in a group of tests named "TestDay" like this:

#include "gtest/gtest.h"

TEST(TestDay, EpochTest) {
  UTCTime utc_time(0L);

  EXPECT_EQ(utc_time.GetYear(), 1970);
  EXPECT_EQ(utc_time.GetMonth(), 1);
  EXPECT_EQ(utc_time.GetDay(), 1);
}

 

If you make this into an executable and run it, it will produce output like this:

[==========] Running 1 tests from 1 test suites.
[----------] Global test environment set-up.
[----------] 1 tests from TestDay
[ RUN      ] TestDay.EpochTest
[       OK ] TestDay.EpochTest (0 ms)
[----------] 1 tests from TestDay (0 ms total)
[----------] Global test environment tear-down
[==========] 1 tests from 1 test suites ran. (0 ms total)
[  PASSED  ] 1 tests.

 

If any test fail, the executable will return an error code and you can fail your build pipeline.

 

gMock

If you want to make integration tests for larger parts of your code (instead of only unit testing specific parts of the code) you will most likely want to mock or stub out parts of the code. Stubs are objects made for simply responding in a determined manner, while mocks in addition will record calls and have expectations on how they are called. What you want to mock away is usually network calls, database calls, file handling, authentication and so on.
Unfortunately, there are not many mock frameworks for C++. gMock (now part of Google Test) does the job for simple cases, but is quickly gets complicated if you use references/pointers, classes without copy constructors or the smart pointers from modern C++. It is however much better than nothing. so please try to make use of it. Installing is just a matter of

`sudo apt-get install libgmock-dev`

using it is just

#include "gmock/gmock.h"

and linking it is just adding to your Makefile LIBSFLAGS

-lgmock

 

A To be able to mock (or stub) out code, you application needs to be able to inject mock (or stub) replacements for that code. Other languages (like Java and C#) are more used to Dependency Injection, but there is no magic to it and no reason for not doing it in C++. Collect database management code (or network, or authentication..) in one class, and instead of using a global singleton or multiple instances of this, make GetDatabaseManager() and SetDatabaseManager() function in your application entry point class. Create a regular instance of your database manager in the application constructor and call SetDatabaseManager() with this instance, and everywhere you have database code, call GetApplication()->GetDatabaseManager() to get hold of it.

Your mock (or stub) database manager should inherit from the regular database manager class, and in the test you will create an instance and call GetApplication()->SetDatabaseManager() to inject your mock (or stub) before calling the code you want to test. Because of how C++ subclassing works, it is important to know that the regular database manager class destructor and all the functions you want to override, needs to be virtual. This will cause a very tiny penalty at regular execution of your application, but well worth it for the cause of testability!

To use some real-world code as example, here is one of my applications using Poco::Net for networking and a gMock class for mocking out the actual network calls. I also include parts of the actual application class and a test. The important parts are shown in red, while the rest may be useful for understanding how the dependency injection works.

Header files (with fancy colours to visualize what goes where in gMock):

class Networking //The actual Networking class
{
public:
  virtual ~Networking() = default; //Needs a virtual destructor
public:
  [[nodiscard]] virtual std::shared_ptr<Poco::Net::HTTPSClientSession> CreateSession(const Poco::URI& uri) const; //Funtions to mock needs to be virtual
  virtual void CallGET(const std::shared_ptr<Poco::Net::HTTPSClientSession>& session, const Poco::URI& uri, const std::string& accept_header) const; //Just an example function. This is how Poco wants a generic REST GET call
};

class NetworkingMock : public Networking //A mock class, inheriting from the actual networking class
{
public:
  virtual ~NetworkingMock() = default;
public:
  MOCK_METHOD(std::shared_ptr<Poco::Net::HTTPSClientSession>, CreateSession, (const Poco::URI& uri), (const));
  MOCK_METHOD(void, CallGET,       (const std::shared_ptr<Poco::Net::HTTPSClientSession>& session,
                                         const Poco::URI& uri,
                                         const std::string& accept_header), (const));
};

class Elspot : public Poco::Util::Application //(The actual application. In this case a Poco application, but it can be whatever)
{
public:
  void SetNetworking(std::shared_ptr<Networking> networking) {m_networking = networking;} //Setter function for Dependency Injection
  [[nodiscard]] std::shared_ptr<Networking> GetNetworking() const {return m_networking;} //Getter function to be able to pick up injected objects
private:
  std::shared_ptr<Networking> m_networking; //The injected object. During tests, this may/will be replaced by a stub or mock object. In this case I've chosen a shared_ptr, but this is all up to you
};

[[nodiscard]] Elspot* GetApp(); //Global function to get hold of the main application object. Implement however you like it

and CPP files:

Poco::AutoPtr<Elspot> g_application = nullptr; //The global singleton of your main application object. Could be unique_ptr, old-style pointer etc
void SetApp(const Poco::AutoPtr<Elspot> application) {g_application = application;} //Setter for the global application object. I've named my application Elspot (it fetches electrical spot prices)
Elspot* GetApp() {return g_application;} //Getter for the global application object. Use ::GetApp instead of passing and keeping pointers, in case the application itself is stubbed or mocked out

void Elspot::init(int /*argc*/, char* /*argv*/[]) //Main entry point for your application. Usually int main(), but this is how Poco does it
{
  ::SetApp(this); //Set application global singleton
  SetNetworking(std::make_shared<Networking>()); //Inject instance of real Networking code. Might be stubbed or mocked later
}


With this setup, you are now ready to make mock tests.