Just about the complex – writing tests with Google C ++ Testing Framework (GTest, GMock)

Deepak k Gupta for this framework in English. And I would like to highlight some points and examples from the video here.

Unfortunately, the author of the video did not deal with the installation of GTest, so I will first give the installation method that I managed to find and test. Ubuntu 20.04 system (tested also on version 18). It assumes that the C++ compiler is already installed.

Since the instruction may become outdated over time, we will analyze the installation in detail so that the principle is clear, then you can adjust the implementation yourself later.

Consider the following code (main.cpp), which should eventually show that the framework has been installed and launched (almost like in test driven development):

#include <gtest/gtest.h>
#include <gmock/gmock.h>

int main(int argc, char **argv)
{
  ::testing::InitGoogleTest(&argc, argv);
  ::testing::InitGoogleMock(&argc, argv);
  
  return RUN_ALL_TESTS();
}

To run it, you need to run the following command from its directory:

g++ main.cpp -o test -lgtest -lgmock -pthread

The executable file test will be created, and not a single error should pop up. When you run ./test, the following message will appear:

[==========] 0 tests from 0 test suites ran. (0 ms total)
[  PASSED  ] 0 tests.

Let’s look at include. These header files need to be installed. The easiest way to do this is with the command:

sudo apt-get install libgtest-dev libgmock-dev # for ubuntu 20
sudo apt-get install google-mock # for ubuntu 18

After installing them, the gtest and gmock folders will appear in the /usr/include/ directory with the header files. However, for full-fledged work, the framework also needs multithreading support. Let’s add it:

sudo apt-get install libtbb-dev

But some header files are not enough to run the example above, you also need an implementation of the functionality that is described in the headers and it is imperative that it be compatible with your system, so you have to compile. It’s not as scary as it used to be. To compile, you need to install the cmake package:

sudo apt-get install cmake

When you installed libgtest-dev a little higher, the googletest and googlemock sources were also added to your system, which can be found in the /usr/src/googletest/ directory

Let’s go there:

cd /usr/src/googletest/

Create a build directory and change to it

sudo mkdir build
cd build

In this directory, run the command

sudo cmake ..

The two dots next to cmake mean to look for the CMakeLists.txt script file in the parent directory. This command will generate a set of instructions for compiling and building the gtest and gmock libraries. After that, it remains to execute:

sudo make

If everything goes well, a new lib directory will be created, where 4 files will be located:

libgmock.a libgmock_main.a libgtest.a libgtest_main.a

These files contain the implementation of the framework functionality and they need to be copied to the directory with the rest of the libraries:

sudo cp lib/* /usr/lib

*For ubuntu 18 the libraries will be in ./googlemock/ and ./googlemock/gtest/
After copying the build directory can be deleted.

Let’s go to the directory with our sample test, run it again:

g++ main.cpp -o test -lgtest -lgmock -pthread
./test

The test should now compile and run.

For those who like to run the code in their favorite IDE, you can create a CMakeLists.txt file in the main.cpp directory with the following content:

cmake_minimum_required(VERSION 3.0)

add_executable(test main.cpp)
target_link_libraries(test gtest gmock pthread)

Let’s get back to the video with some of my comments.

As in all EPs, they first show the so-called. Hello world. For GTest, it might look like this:

#include <gtest/gtest.h>

using namespace std;

TEST(TestGroupName, Subtest_1) {
  ASSERT_TRUE(1 == 1);
}

TEST(TestGroupName, Subtest_2) {
  ASSERT_FALSE('b' == 'b');
  cout << "continue test after failure" << endl;
}

int main(int argc, char **argv)
{
  ::testing::InitGoogleTest(&argc, argv);

  return RUN_ALL_TESTS();
}

Here everything is intuitively clear. Almost. The framework makes extensive use of macros. In the TEST macro, the first argument in brackets means the name of a group of tests united by a common logic. The second argument is the name of the particular test in the subgroup.
After starting in the terminal, you will see which test was successful and which was not:

[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from TestGroupName
[ RUN      ] TestGroupName.Subtest_1
[       OK ] TestGroupName.Subtest_1 (0 ms)
[ RUN      ] TestGroupName.Subtest_2
/home/gtests/main.cpp:10: Failure
Value of: 'b' == 'b'
  Actual: true
Expected: false
[  FAILED  ] TestGroupName.Subtest_2 (0 ms)
[----------] 2 tests from TestGroupName (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] TestGroupName.Subtest_2

 1 FAILED TEST

Further, to simplify, I will show only the tests themselves, without headers and main functions.

ASSERT_TRUE and ASSERT_FALSE are also macros that implement the so-called. “assertions” that the framework will check

The assertions are:

  • successful

  • unsuccessful, but non-fatal (non-fatal failure)

  • unsuccessful, fatal (fatal failure)

The differences between the second and third options can be understood by looking at the test code above. The ASSERT_FALSE and ASSERT_TRUE macros abort the test execution (fatal failure) and the following command will no longer be called.

The same behavior can be observed in the ASSERT_EQ(param1, param2) macro, which compares its two arguments for equality:

TEST(TestGroupName, Subtest_1) {
  ASSERT_EQ(1, 2);
  cout << "continue test" << endl; // не будет выведено на экран
}

The EXPECT_EQ macro works differently – in case of failure, the execution of the code after it will continue

TEST(TestGroupName, Subtest_1) {
  EXPECT_EQ(1, 2); // логи покажут тут ошибку
  cout << "continue test" << endl; // при этом будет выведено на экран данное сообщение
}

The following endings can be used for ASSERT_ and EXPECT_:

EQ
NE – Not equal
LT – Less Than
LE-LessThanEqual
GT – Greater Than
GE – Greater Than Equal

There are actually more endings, because when testing, not only integers are compared. For real numbers, strings, predicates, examples of endings can be found in https://habr.com/ru/post/119090/

Further, the author of the video talks about the unit test scheme, that each test consists of three stages:

  • Arrange – prepare all the necessary initial data for the test

  • Act – run the method or function being checked

  • Assert – check result

For example:

TEST(TestGroupName, increment_by_5)
{
  // Arrange
  int value = 100;
  int increment = 5;

  // Act
  value = value + increment;

  // Assert
  ASSERT_EQ(value, 105);
}

There are others besides the TEST macro. And now we will get to know them.

Let’s say we have such a class

class MyClass
{
  string id;

public:
  MyClass(string _id) : id(_id) {}
  string GetId() { return id; }
};

Let’s write a test that checks the work of the constructor and getter:

TEST(TestGroupName, increment_by_5)
{
  // Arrange
  MyClass mc("root");

  // Act
  string value = mc.GetId();

  // Assert
  EXPECT_STREQ(value.c_str(), "root"); // строки сравнивают с _STREQ
}

In real development, a class is rarely limited to one method, so to test each method, you will have to initialize the class over and over again, which is very inconvenient. For such a case, there is Test Fixtures.

For the convenience of understanding the principle, let’s add another method to the public section that adds a string to the end of the existing one:

void AppendToId(string postfix) { id += postfix; }

Task: to test the work of both methods, and, if possible, avoid code duplication. Let’s start with what the tests will look like:

TEST_F(MyClassTest, init_class)
{
  // Act
  string value = mc->GetId();

  // Assert
  EXPECT_STREQ(value.c_str(), "root");
}

TEST_F(MyClassTest, append_test)
{
  // Act
  mc->AppendToId("_good");
  string value = mc->GetId();

  // Assert
  EXPECT_STREQ(value.c_str(), "root_good");
}

In both tests, we don’t get distracted by class “initialization” and don’t worry about freeing memory. Moreover, the launch of a new test is accompanied by the creation of an instance of the class from a “clean slate”.

“Initialization” will happen once in a new (helper) class inherited from testing::Test:

struct MyClassTest : public testing::Test {
  MyClass *mc;

  void SetUp() { mc = new MyClass("root"); } // аналог конструктора
  void TearDown() { delete mc; } // аналог деструктора
};

In the SetUp() method, we set the initial conditions, in TearDown() we clean up after ourselves.
To make it work, we change TEST to TEST_F and specify the name of the auxiliary class – MyClassTest – as the first argument. Everything, you can test and not be distracted by trifles.

We finally come to where it all started – EXPECT_CALL and mocks.

Let’s take a look at this program:

#include <string>

class Mylib {
public:
  void setVoltage(int v) {
    // complex logic
  }
};

class Myapp {
  Mylib *mylib_;

public:
  explicit Myapp(Mylib *mylib) : mylib_(mylib){};
  
  void run(const std::string& cmd) {
    if (cmd == "ON") {
      mylib_->setVoltage(220);
    } else if (cmd == "OFF") {
      mylib_->setVoltage(0);
    }
  }
};

int main() {
  Mylib mylib;
  Myapp app(&mylib);
  app.run("ON");
}

The task is to write a test: if “ON” is passed to the run method, then setVoltage(220) should be called, i.e. exactly setVoltage and without fail with the “220” argument. Moreover, what will be executed or not executed inside setVoltage (220) should not interest us.

To do this, you need to push yourself a little. Let’s add an interface for our library (Mylib class):

class MylibInterface {
public:
  virtual ~MylibInterface() = default;
  virtual void setVoltage(int) = 0;
};

and inherit from it:

class Mylib : public MylibInterface {
public:
  void setVoltage(int v) {
    // complex logic
  }
};

This will give us the opportunity to replace the Mylib field in the Myapp class and the type of the argument in the constructor with MylibInterface

At the same time, we note that the program logic has not changed at all, but instead of a specific Mylib class, we can connect any other class that implements the MylibInterface interface. This is what we will use. Let’s create the MylibMock class, also inherited from MylibInterface with the following content:

class MylibMock : public MylibInterface {
public:
  ~MylibMock() override = default;
  MOCK_METHOD1(setVoltage, void(int));
};

at the same time we include two header files:

#include <gmock/gmock.h>
#include <gtest/gtest.h>

Let’s look at the macro
MOCK_METHOD1(setVoltage, void(int));

The first argument is the name of the very method that we expect to be executed in our future test. Next comes the signature of this method. The number 1 in the macro name means the number of arguments for the setVoltage method – one.
*In new versions of gmock, you can use this notation
MOCK_METHOD(void, setVoltage, (int v), (override));

Now everything is ready to write the test:

TEST(MylibTestSuite, mock_mylib_setVoltage) {
  MylibMock mylib_mock;
  Myapp myapp_mock(&mylib_mock);

  EXPECT_CALL(mylib_mock, setVoltage(220)).Times(1);

  myapp_mock.run("ON");
}

You can read from the end of the test: when you run the run method with the “ON” argument, setVoltage is expected to be called once with the argument 220.

To run the test(s) you need to write

int main(int argc, char **argv) {
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}
Full code under the spoiler
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

class MylibInterface {
public:
  virtual ~MylibInterface() = default;
  virtual void setVoltage(int) = 0;
};

class MylibMock : public MylibInterface {
public:
  ~MylibMock() override = default;
  MOCK_METHOD1(setVoltage, void(int));
};

class Mylib : public MylibInterface {
public:
  void setVoltage(int v) {
    // complex logic
  }
};

class Myapp {
  MylibInterface *mylib_;

public:
  explicit Myapp(MylibInterface *mylib) : mylib_(mylib){};
  
  void run(const std::string& cmd) {
    if (cmd == "ON") {
      mylib_->setVoltage(220);
    } else if (cmd == "OFF") {
      mylib_->setVoltage(0);
    }
  }
};

TEST(MylibTestSuite, mock_mylib_setVoltage) {
  MylibMock mylib_mock;
  Myapp myapp_mock(&mylib_mock);

  EXPECT_CALL(mylib_mock, setVoltage(220)).Times(1);

  myapp_mock.run("ON");
}

int main(int argc, char **argv) {
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

That’s all for now, I hope it was clear and interesting. In fact, the Google C++ Testing Framework contains many other useful features that make testing easier. I would be very glad if someone shares their experience of using gtest / gmock in their practice.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *