Всем привет. Недавно по работе возникла потребность разобраться с созданием новых тестов на GTest/GMock. Конкретно мой вопрос был связан с его конструкциями типа EXPECT_CALL и моками - что это за магические штуки и как они работают? Однако, по мере изучения выяснились и другие интересные вещи, с которыми хотел бы поделиться.

Первым делом ответы стал искать на Хабре. Здесь зачастую авторы стараются рассказывать сложные вещи простым языком. Однако по данной теме найденные публикации оказались для меня либо не сильно информативными, либо рассчитанными на очень подготовленных читателей. Так в [1] подача материала была больше похожа на справочник, который хорошо иметь под рукой, но уже после наработки некоторого опыта с фреймворком. В [2] приведен способ установки GTest на Ubuntu 11, который, как выяснилось требует дополнительных действий. Быстрый старт в [3] оказался нереально быстрым и коротким и заточенным под Visual Studio 2008\2010. В [4 и 5] можно найти очень серьезные работы по юнит-тестированию и новичку там будет непросто понять идею, когда первый пример начинается с тестирования класса для работы с сетевым соединением и базой данных.

Поиск по просторам интернета привел на серию интересных видео от Deepak k Gupta по данному фреймворку на английском языке. И некоторые моменты и примеры из видео я хотел бы осветить тут.

К сожалению автор видео не стал разбираться с установкой GTest, поэтому приведу вначале тот способ установки, что мне удалось найти и апробировать. Система Ubuntu 20.04 (проверялось также и на 18-й версии). Предполагается, что компилятор С++ уже установлен.

Так как инструкция со временем может устареть, то разберем установку подробно, чтобы был понятен принцип, тогда реализацию сможете сами потом скорректировать.

Способ №1

Рассмотрим такой код (main.cpp), который в итоге должен показать, что фреймворк установился и запустился (почти как в 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();
}

Для его запуска нужно выполнить из его директории команду:

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

Будет создан исполняемый файл test, при этом не должно выскочить ни единой ошибки. При запуске ./test появится сообщение:

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

Посмотрим на include. Эти заголовочные файлы нужно установить. Проще всего это сделать командой:

sudo apt-get install libgtest-dev libgmock-dev # для ubuntu 20
sudo apt-get install google-mock # для ubuntu 18

После их установки в каталоге с заголовочными файлами /usr/include/ появятся папки gtest и gmock. Однако, для полноценной работы фреймворку нужна еще поддержка многопоточности. Добавим ее:

sudo apt-get install libtbb-dev

Но одних заголовочных файлов мало для запуска примера выше, нужна еще реализация того функционала, что описана в заголовках и обязательно, чтобы она была совместима с вашей системой, поэтому придется компилировать. Это уже не так страшно, как было когда-то. Для компиляции понадобится установить пакет cmake:

sudo apt-get install cmake

Когда вы установили чуть выше libgtest-dev - в вашу систему также добавились исходники googletest и googlemock, которые можно найти в директории /usr/src/googletest/.

Идем туда:

cd /usr/src/googletest/

Создаем каталог для сборки и переходим в него:

sudo mkdir build
cd build

В этом каталоге запускаем команду:

sudo cmake ..

Две точки рядом с cmake означают, что нужно искать файл сценария CMakeLists.txt в родительском каталоге. Эта команда сгенерирует набор инструкций для компиляции и сборки библиотек gtest и gmock. После чего останется выполнить:

sudo make

Если все пройдет успешно, то будет создан новый каталог lib, где будут находится 4 файла:

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

Эти файлы содержат реализацию функционала фреймворка и их нужно скопировать в каталог к остальным библиотекам:

sudo cp lib/* /usr/lib

*Для ubuntu 18 библиотеки будут находится в ./googlemock/ и ./googlemock/gtest/
После копирования каталог build можно удалить.

Перейдем в директорию с нашим примером теста, снова запустим его:

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

Теперь тест должен скомпилироваться и запуститься.

Для любителей запускать код в своей любимой IDE можно в директории с main.cpp создать файл CMakeLists.txt такого содержания:

cmake_minimum_required(VERSION 3.0)

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

Способ №2 (чуть проще)

Спасибо за подсказку sa2304. Его отличие заключается в том, что исходники GTest устанавливаются в каталог с проектом из git репозитория googletest. Для начала нужно инициализировать пустой репозиторий в своем каталоге с файлом main.cpp:

git init

После чего клонировать репозиторий:

git clone https://github.com/google/googletest.git

В вашем каталоге появится новая директория googletest с исходниками фреймворка. Дальше создаем файл CMakeLists.txt с таким содержимым:

cmake_minimum_required(VERSION 3.0)

project("gtest")

enable_testing()
add_subdirectory(googletest)
add_executable(test_app main.cpp)
target_link_libraries(test_app gtest_main gmock_main)

И в файле main.cpp вместо

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

указываем путь к "локальным" заголовочным файлам:

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

Дальше собственно вернемся к видео с некоторыми моими комментариями.

Как и во всех ЯП вначале показывают т.н. "Hello world". Для GTest он может выглядеть так:

#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();
}

Здесь все интуитивно ясно. Ну почти. Фреймворк активно использует макросы. В макросе TEST первый аргумент в скобках означает название группы тестов, объединенных общей логикой. Второй аргумент - название конкретного теста в подгруппе.

После запуска в терминале будет видно какой тест прошел успешно, какой нет:

[==========] 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

Дальше для упрощения буду показывать только сами тесты, без заголовков и main функции.

ASSERT_TRUE и ASSERT_FALSE - тоже макросы, реализующие т.н. "утверждения", которые будет проверять фреймворк.

Утверждения бывают:

  • успешные (success);

  • неудачные, но нефатальные (non-fatal failure);

  • неудачные, фатальные (fatal failure).

Отличия второго от третьего варианта можно понять, взглянув на код теста выше. Макросы ASSERT_FALSE и ASSERT_TRUE прерывают выполнение теста (fatal failure) и идущая следом команда уже не будет вызвана.

Такое же поведение можно наблюдать у макроса ASSERT_EQ(param1, param2) сравнивающего два своих аргумента на равенство:

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

По другому работает макрос EXPECT_EQ - в случае неудачи выполнение кода после него продолжится:

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

Для ASSERT_ и EXPECT_ можно использовать следующие окончания:

EQ - Equal
NE - Not Equal
LT - Less Than
LE - Less than or Equal to
GT - Greater Than
GE - Greater than or Equal to

Окончаний на самом деле больше, т.к. при тестировании сравнивают не только целые числа. Для вещественных чисел, строк, предикатов примеры окончаний можно подглядеть в https://habr.com/ru/post/119090/.

Дальше автор видео рассказывает про схему юнит-теста, что каждый тест состоит из трех этапов:

  • Arrange - подготовить все необходимые исходные данные для теста.

  • Act - запустить проверяемый метод или функцию.

  • Assert - сверить результат.

Например:

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

  // Act
  value = value + increment;

  // Assert
  ASSERT_EQ(value, 105);
}

Помимо макроса TEST есть и другие. И сейчас мы с ними познакомимся.

Допустим у нас есть такой класс:

class MyClass
{
  string id;

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

Напишем тест, проверяющий работу конструктора и геттера:

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

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

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

В реальной разработке одним методом у класса редко ограничиваются, поэтому для тестирования каждого метода придется раз за разом инициализировать класс, что очень неудобно. Для такого случая есть Test Fixtures.

Для удобства понимания принципа добавим в public секцию еще один метод, добавляющий строку в конец имеющейся:

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

Задача: протестировать работу обоих методов, и по-возможности избежать дублирования кода. Начнем с того как будут выглядеть тесты:

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");
}

В обоих тестах мы не отвлекаемся на "инициализацию" класса и не беспокоимся об освобождении памяти. Более того, запуск нового теста сопровождается созданием экземпляра класса с "чистого листа".

"Инициализация" будет происходить один раз в новом (вспомогательном) классе, унаследованном от testing::Test:

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

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

В методе SetUp() мы задаем начальные условия, в TearDown() убираем за собой.

Чтобы все заработало мы меняем TEST на TEST_F и первым аргументом указываем имя вспомогательного класса - MyClassTest. Все, можно тестировать и не отвлекаться на мелочи.

Подходим наконец к тому, с чего все началось - EXPECT_CALL и моки.

Взглянем на такую программу:

#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");
}

Задача написать тест: если методу run передать "ON", то должен произойти вызов setVoltage(220), т.е. именно setVoltage и непременно с аргументом "220". Причем что там будет выполнено или не выполнено внутри setVoltage(220) нас не должно интересовать.

Чтобы такое осуществить нужно немного поднапрячься. Добавим интерфейс для нашей библиотеки (класса Mylib):

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

и унаследуемся от него:

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

Это даст нам возможность заменить в классе Myapp поле Mylib и тип аргумента в конструкторе на MylibInterface.

При этом заметим, что логика программы ничуть не изменилась, зато вместо конкретного класса Mylib мы можем подключить любой другой, реализующий интерфейс MylibInterface. Этим мы и воспользуемся. Создадим класс MylibMock, тоже унаследованный от MylibInterface такого содержания:

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

заодно подключим два заголовочных файла:

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

Обратим внимание на макрос:

MOCK_METHOD1(setVoltage, void(int));

Первым аргументом идет имя того самого метода, который мы ожидаем что будет выполнен в нашем будущем тесте. Далее идет сигнатура этого метода. Цифра 1 в названии макроса означает число аргументов у метода setVoltage - один.

*В новых версиях gmock можно использовать такую запись:

MOCK_METHOD(void, setVoltage, (int v), (override));

Теперь все готово к написанию теста:

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");
}

Читать можно с конца теста: при запуске метода run с аргументом "ON" ожидается однократный вызов setVoltage с аргументом 220.

Чтобы запустить тест (тесты) нужно написать:

int main(int argc, char **argv) {
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}
Полный код под спойлером
#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();
}

На этом пока все, надеюсь было понятно и интересно. На самом деле Google C++ Testing Framework содержит много других полезных фишек, упрощающих тестирование. Буду очень рад если кто-нибудь поделится опытом применения gtest/gmock в своей практике.

Комментарии (10)


  1. syrus_the_virus
    26.05.2022 15:24
    -4

    Я не понимаю, какой сейчас в этом смысл? Всё зарубежные компании, весь зарубежный софт, связанный с этим API - это же настолько всё зыбко, ненадёжно, нестабильно. Зачем завязывать себя, свой код, свои программные продукты на это? Где здравый смысл?


  1. fregate
    26.05.2022 17:33

    что это за магические штуки и как они работают?

    Надеялся, что быстро и на пальцах расскажут, как это работает под капотом, но оказалось, что это ещё один небольшой справочник про синтаксис


    1. KudryashovDA Автор
      26.05.2022 18:43

      Мне тоже интересно внутреннее устройство, но пока задача была разобраться с синтаксисом и как оно работает, т.е. применяется. Если по TEST и TEST_F можно и так догадаться, то с моками мне так и не попалось простых примеров и объяснения принципа построения теста. Про TEST могу предположить что там под капотом, мы нечто похожее делали на курсах - свою микротестовую систему на макросах. В принципе там несложный код, главное покрыть по максимуму разные типы данных (простые и контейнеры stl) и чтобы еще при ошибке выводилось их содержимое. А как макросы перевести в названия внутренних функций и их аргументов - это для меня до сих пор магия.


  1. sergio_nsk
    27.05.2022 05:11

    LE - LessThanEqual
    GR - GreaterThanEual

    Меньше, чем равно? Больше, чем равно? Что это?

    EXPECT_STREQ(value.c_str(), "root");

    Зачем так длинно и не оптимально? Почему не EXPECT_EQ(value, "root");?


    1. KudryashovDA Автор
      27.05.2022 10:43

      Меньше, чем равно? Больше, чем равно? Что это?

      Это машинальное копирование, не подумав о тех кто будет читать. Такая запись приводится в некоторых источниках и возможно у англоязычных IT пользователей звучит естественно. Спасибо за замечание, в тексте подкорректировал расшифровку.

      Зачем так длинно и не оптимально? Почему не EXPECT_EQ(value, "root");?

      Кто ж, как говорится, знал что и так можно... Сейчас у себя проверил, действительно работает. Могу только предположить, что изначально автор видео, откуда пошел этот пример, написал так, т.к. использовал древнюю версию gtest.


      1. fregate
        27.05.2022 12:57

        Ну странно, конечно. В официальной доке пишут, что EXPECT_EQ, если использовать на указателях будет сравнивать то, что они указывают на одну и ту же область памяти, но не саму память, а вот EXPECT_STREQ будет, как раз сравнивать память.

        Либо что-то поменялось в их королевстве и они не изменили документацию, либо надо им писать с примерами...


        1. naviUivan
          27.05.2022 14:56

          В данном случае value имеет тип std::string, а не указатель на C-строку


  1. sa2304
    27.05.2022 21:40
    +1

    sudo make

    Не стоит делать этого от имени администратора, мало ли чего в ходе сборки выполняется :)

    Есть простой способ подключить googletest к любому проекту:

    git submodule add https://github.com/google/googletest.git

    а в CMakeLists.txt пишем:

    enable_testing()
    add_subdirectory(googletest)
    add_executable(test_app test_main.cpp)
    target_link_libraries(test_app gtest_main)

    так и собираем всегда свежую версию googletest, и проблем с линковкой не возникнет из-за разных флагов сборки проекта и библиотеки.


    1. KudryashovDA Автор
      27.05.2022 21:43
      +1

      Спасибо за совет. После проверки у себя добавил его в публикацию с небольшими дополнениями.


    1. enree
      28.05.2022 10:31

      Простой способ подключать внешние библиотеки - через conan. Собирать каждый раз то, что не меняется, имхо, так себе удовольствие