Просто о сложном — пишем тесты с Google C++ Testing Framework (GTest, GMock)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

Рассмотрим такой код (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)

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

Как и во всех ЯП вначале показывают т.н. "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 - LessThan
LE - LessThanEqual
GT - GreaterThan
GE - GreaterThanEqual

Окончаний на самом деле больше, т.к. при тестировании сравнивают не только целые числа. Для вещественных чисел, строк, предикатов примеры окончаний можно подглядеть в 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 в своей практике.

Источник: https://habr.com/ru/post/667880/


Интересные статьи

Интересные статьи

Ни для кого не секрет, что от того, как активно вы заполняете страницу своего бизнеса в сервисе Google My Business, зависит ее позиция в локальном поиске.То есть, постоянная работа со страницей позвол...
Привет, я Андрей, работаю Flutter разработчиком в компании Финам.Продолжим развивать сервис Umka. На примере реализации кода для проведения "экзамена" мы познакомимся с в...
В этой статье мы расскажем, как оптимизировать крупный проект в «Битрикс24» и увеличить его производительность в 3 раза, изменяя настройки MySQL и режим питания CPU. Дано Корпоративн...
Материал, первую часть перевода которого мы сегодня публикуем, посвящён новым стандартным возможностям JavaScript, о которых шла речь на конференции Google I/O 2019. В частности, здесь мы поговор...
Небольшое вступление: Идея написания собственного ядра появилась после прохождения школы-семинара по цифровой схемотехнике в городе Томске. На данном мероприятии проводилось знакомство с текущим...