Nakerdev logo
Published on

II. Unit Testing Guide in C++: GTest as Testing Framework

Authors
  • avatar
    Name
    Antonio Sánchez
    Twitter

This article explains why I consider GoogleTests to be the best unit testing framework for C++ projects and some considerations to keep in mind before embarking on the journey of creating unit tests with it.

This article is the continuation of a series of three articles. If you haven't read the first part, I recommend doing so before starting this one.

GoogleTests as Unit Testing Framework

If we search online for unit testing frameworks for C++ projects, we'll find quite a few options, but most of them have poor documentation and little community support. Of all the options available, the one that shines the most in my opinion is GoogleTests (GTests).

The advantages I see compared to other testing frameworks are:

  • Clear syntax: If you come from higher-level languages, you might think I'm crazy, but GTests compared to other testing frameworks has decent syntax.
  • Built-in Mock support: GTests relies on another library called gMock to help us with test doubles. Creating test doubles in C++ is not as trivial as in higher-level languages, but gMock solves this need quite well.
  • Good assertion framework: GTests provides us with a good assertion API (https://google.github.io/googletest/reference/matchers.html) for our unit tests. It also allows us to create our own custom assertion functions very easily (https://google.github.io/googletest/reference/matchers.html#defining-matchers). This is very useful in C++ due to its wide variety of types.
  • Integration with different build systems: GTests works for projects with Visual Studio, CMake, or Makefile
  • CI Integration: Reports automatically generated by GTests use the JUnit-style XML format, this is quite important because it's the format used by most CI systems like Gitlab Pipelines or Github actions to interpret reports.

Given-When-Then VS Arrange-Expect-Act

Given-When-Then or Arrange-Act-Assert (not to be confused with Arrange-Expect-Act from the title) are well-known patterns in unit testing for structuring tests. They basically define three main blocks in the test: The setup, the code to be tested, and the assertions.

#include "gtest/gtest.h"
#include "gmock/gmock.h"

TEST(TestCaseName, TestName)
{
    //GIVEN
    int num1 = 1;
    int num2 = 1;

    //WHEN
    int result = num1 + num2;

    //THEN
    ASSERT_EQ(result, 2);
}

All good so far, as long as we're not testing an artifact where we need to use test doubles.

Let's go back to the previous example, the one we used to talk about polymorphism without interfaces. Suppose we have the following implementation of the user service:

#include "pch.h"
#include "UserService.h"

UserService::UserService(std::shared_ptr<UserRepository> userRepository) :
    _userRepository(userRepository)
{
}

void UserService::Create(const std::shared_ptr<User>& user)
{
    if(_userRepository->Exist(user)) return;
    _userRepository->Save(user);
}

The first approach to a unit test might be the following:

#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include "FakeUserRepository.h"
#include "UserService.h"

class UserServiceTests : public ::testing::Test
{
protected:
    std::shared_ptr<FakeUserRepository> userRepository = std::make_shared<FakeUserRepository>();
    UserService* userService;

    void SetUp() override
    {
        userService = new UserService(userRepository);
    }

    void TearDown() override
    {
        delete userService;
    }
};

TEST_F(UserServiceTests, Saves_User)
{
    //GIVEN
    std::shared_ptr<User> user = std::make_shared<User>(1);
    ON_CALL(*userRepository, Exist(user))
        .WillByDefault(testing::Return(false));

    //WHEN
    userService->Create(user);

    //THEN
    EXPECT_CALL(*userRepository, Save(user))
        .Times(testing::Exactly(1));
}

However, if we run the above test, we'll see red on the screen because the test doesn't consider that the repository's "Save" method was called once.

We must keep in mind that any configuration we make on the test double must be done before executing the code that exercises it (in this case userService->Create(user)).

TEST_F(UserServiceTests, Saves_User)
{
    //ARRANGE
    std::shared_ptr<User> user = std::make_shared<User>(1);
    ON_CALL(*userRepository, Exist(user))
        .WillByDefault(testing::Return(false));

    //EXPECT
    EXPECT_CALL(*userRepository, Save(user))
        .Times(testing::Exactly(1));

    //ACT
    userService->Create(user);
}

For this reason, the Arrange-Expect-Act convention fits better with the design of unit tests in this language, or at least with GTests and gMock.

ON_CALL VS EXPECT_CALL

In the context of gMock, ON_CALL is less used compared to EXPECT_CALL. Both are used to define the behavior of a test double, but there are key differences:

  • ON_CALL defines the behavior of a method without setting expectations on its invocation.
  • EXPECT_CALL defines the behavior and also sets expectations about the number, order of invocations, and parameters of the method.

Although EXPECT_CALL seems more complete, it can be counterproductive if used excessively, as it adds unnecessary constraints to the test. This can make maintenance and code flexibility more difficult, as any change in implementation could break the tests.

It's recommended to use ON_CALL by default and resort to EXPECT_CALL only when it's necessary to verify that a specific call is made.

Custom Matchers

Matchers are used to test if two values are equal when calling a test double. It's a necessary practice when writing unit tests.

EXPECT_CALL(*mockObj, Foo(::testing::Eq(42)))
        .WillOnce(::testing::Return(false));

In the above example, we're checking that the Foo method is called with a parameter that is exactly 42, if this is met, the function will return false when called.

gMock defines a series of predefined matchers in the library (https://google.github.io/googletest/reference/matchers.html) that are very useful but sometimes not powerful enough.

In C++, it's common practice to assign alternative names to existing types using the reserved word typedef, such as the following type defined in a Windows library:

typedef unsigned char       BYTE;

Note that the BYTE type is still an 8-bit integer that cannot contain negative values, but the library uses BYTE instead of directly using unsigned char.

For this reason, and because each library can define its own data types, we must create our own custom matchers if we want to have solid unit tests.

gMock allows us to define our own matchers easily (https://google.github.io/googletest/reference/matchers.html#defining-matchers). The only thing we need to be clear about is what we need to compare to assert that two data types are equal.

MATCHER_P(IsOptionalEqualTo, expected_value, "") {
    return arg.has_value() && arg.value() == expected_value;
}

The above custom matcher allows us to assert that two optional values (std::optional) are equal. An example of usage could be:

std::optional<std::string> expectedParam = "param_value";
EXPECT_CALL(*mockObj, Foo(IsOptionalEqualTo(expectedParam)))

The reserved words that allow us to create custom matchers (MATCHER, MATCHER_P or MATCHER_P2) are macros, so they cannot be used within functions or classes. We must also keep in mind that the body of our matchers must be pure functions that don't depend on anything external to make comparisons and don't produce side effects in their execution.

Custom Actions

Actions are used to define what a test double should do when executed. Going back to the previous example where we talked about matchers:

EXPECT_CALL(*mockObj, Foo(::testing::Eq(42)))
        .WillOnce(::testing::Return(false));

::testing::Eq is the matcher and ::testing::Return is the action.

Just as gMock defines a series of built-in matchers in the library, it also defines a series of actions that allow us to cover most scenarios in our tests: https://google.github.io/googletest/reference/actions.html.

However, sometimes these built-in actions in the library are not powerful enough, and we'll need to create our own custom actions.

A common case where this practice is necessary occurs when trying to create test doubles for functions that use output parameters. In these cases, like with matchers, these parameters are not types supported by the library's actions.

Suppose we have the following test double:

class FakeDataEncryptionService : public DataEncryptionService
{
public:
    MOCK_METHOD(bool, Decrypt, (const std::string& encryptedData, BYTE* decryptedData, DWORD* decryptedDataLen), (override));
};

The Decrypt method receives three parameters, of which the first is input and the last two are output (where the function's output will be stored).

To work with test doubles that use output parameters, gMock defines a series of actions such as: SaveArg<N>(pointer), SaveArgPointee<N>(pointer), SetArgReferee<N>(value) or SetArgPointee<N>(value) that allow us to assign or save parameter values of a function.

So we might interpret that to work with our test double we should do something like this:

std::wstring encryptedData = L"enctypted_data";
BYTE* decryptedData = (BYTE*)("{id: 1}");
EXPECT_CALL(*encryptionService, Decrypt(encryptedData, ::testing::_, ::testing::_))
    .WillOnce(testing::DoAll(
        ::testing::SetArgPointee<1>(decryptedData),
        ::testing::Return(true)));

The above code assigns the value of the decryptedData variable to parameter 1 (starting from index 0), but just as with matchers, the BYTE* type is not a type directly supported by ::testing::SetArgPointee<N> and when running the test we would see an error on the screen.

Error    C2440    '=': cannot convert from 'const A' to 'BYTE'

The error tells us that type A cannot be converted to type BYTE, the compiler cannot infer the type of argument 1 of the Decrypt function because it's a type it doesn't know.

The action SetArgPointee is equivalent to writing:

*arg = param;

Where arg is a pointer to parameter 1 of the Decrypt function, arg is a data type that cannot be inferred by the compiler.

The solution, as with matchers, is to create our custom action to work with this data type:

ACTION_P(AssignDecryptedDataParam, param)
{
    BYTE* destPtr = static_cast<BYTE*>(arg1);
    BYTE* sourcePtr = static_cast<BYTE*>(param);

    auto byteArrayLen = wcslen(reinterpret_cast<const wchar_t*>(sourcePtr)) * sizeof(wchar_t);

    memcpy(destPtr, sourcePtr, byteArrayLen);
}

We ensure that both the function argument we're working with and the value we want to assign to it are of type BYTE* and finally copy the data from one memory region to another.

This specific case is somewhat more complex than usual since a BYTE* cannot be directly assigned to another BYTE*, but in summary, we must manually convert the type for the compiler:

ACTION_P(AssignMyType, param) { *static_cast<MyType*>(arg1) = param; }

Finally, we replace the use of the built-in gMock action with our custom action:

std::wstring encryptedSerializedUser = L"enctypted_data";
BYTE* serializedUser = (BYTE*)("{id: 1}");
EXPECT_CALL(*encryptionService, Decrypt(encryptedSerializedUser, ::testing::_, ::testing::_))
    .WillOnce(testing::DoAll(
        AssignDecryptedDataParam(serializedUser),
        testing::Return(true)));

How to Test Code That Uses Callbacks

At one point, I found it difficult to figure out how to do unit testing on code that uses callbacks, which is why I'm leaving this small space reserved for it.

To exemplify this, let's assume the following practical case; A class that acts as an event center, this class subscribes to different events. When an event is received, a push notification is sent with the event message. Note that events are received asynchronously and uncontrollably. A callback is used to capture the events.

EventListener.h

#pragma once
#include <string>

class EventListener
{
public:
    virtual void ListenMessages(
        void (*handler)(void* context, const std::string& eventMessage),
        void* context) = 0;
};

PushNotificationService.h

#pragma once
#include <string>

class PushNotificationService
{
public:
    virtual void Push(const std::string& message) = 0;
};

EventsCenter.cpp (I've omitted the header file for this class)

#include "pch.h"
#include "EventsCenter.h"

static void MessageReceivedHandler(void* context, const std::string& message)
{
    EventsCenter* self = static_cast<EventsCenter*>(context);
    self->PushMessage(message);
};

EventsCenter::EventsCenter(
    std::shared_ptr<PushNotificationService> pushNotificationService,
    std::shared_ptr<EventListener> systemEventListener):
    _pushNotificationService(pushNotificationService),
    _systemEventListener(systemEventListener) {}

void EventsCenter::SubscribeToEvents()
{
    _systemEventListener->ListenMessages(MessageReceivedHandler, this);
    //_serverEventListener->Listen...
    //...
}

void EventsCenter::PushMessage(const std::string& message)
{
    _pushNotificationService->Push(message);
}

To be able to make a unit test for the above code, we must capture the first argument of the _systemEventListener->ListenMessages method. Once we have it, we can use it in our test to test the complete flow of the code. To be able to do this, gMock provides us with an action called Invoke (https://google.github.io/googletest/reference/actions.html#using-a-function-functor-or-lambda-as-an-action)

Here's the test:

//includes omitted.

class EventsCenterTests : public ::testing::Test
{
protected:
    std::shared_ptr<FakePushNotificationService> pushNotificationService = std::make_shared<FakePushNotificationService>();
    std::shared_ptr<FakeEventListener> systemEventListener = std::make_shared<FakeEventListener>();
    EventsCenter* eventsCenter;

    void (*callBackHandler)(void* context, const std::string& notification) = nullptr;
    void* callBackContext = nullptr;

    void SetUp() override
    {
        eventsCenter = new EventsCenter(pushNotificationService, systemEventListener);
    }

    void TearDown() override
    {
        delete eventsCenter;
    }

public:

    void captureListenMessagesCallBack(
        void (*handler)(void* context, const std::string& notification),
        void* context)
    {
        callBackHandler = handler;
        callBackContext = context;
    }
};

TEST_F(EventsCenterTests, Sends_Push_Notification_When_System_Event_Received)
{
    EXPECT_CALL(*systemEventListener, ListenMessages(testing::_, testing::_))
        .WillOnce(testing::Invoke(this, &EventsCenterTests::captureListenMessagesCallBack));
    eventsCenter->SubscribeToEvents();

    std::string systemEventMessage = "Successfully scheduled Software Protection service for re-start at 2124-04-29T13:20:44Z. Reason: RulesEngine.";
    EXPECT_CALL(*pushNotificationService, Push(systemEventMessage))
        .Times(::testing::Exactly(1));

    callBackHandler(callBackContext, systemEventMessage);
}