Использование Google Testing Framework

Хороший код должен быть протестирован

Debugging sucks! Testing rocks!

Если код не протестирован, то он не хорош :)

Системы тестирования

При регулярном написании тестов разработчик сталкивается с рядом специфических проблем:

  • Код теста содержит в себе много достаточно однотипных проверок. Необходимо максимально упростить и сократить время написания этого проверочного кода.
  • Каждый тестируемый модуль / класс как правило требует проверки различных аспектов своего поведения. Необходимо максимально упростить процессы написания, организации и управления фрагментами кода.
  • Для проведения тестов часто требуется создание специального тестового окружения: файлы с входными данными, сконструированные и приведённые в требуемое состояние объекты других классов / модулей, и т. п. После прогона теста и анализа результатов, тестовое окружение необходимо очистить либо вернуть в начальное состояние для прогона следующего теста.

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

В рамках данного курса студентам предлагается использовать систему тестирования Google Testing Framework (далее, для краткости, GoogleTests)

Ключевые понятия

Кратко перечислим базовые строительные элементы GoogleTests.

  • Assert | Утверждение — это выражение-проверка, результатом выполнения которого могут быть:
    • Success | Успех — проверка выполнена успешно.
    • Nonfatal Failure | Отказ — проверка не выполнена, но данный отказ не является критическим, выдается диагностическое сообщение, и выполнение теста продолжается.
    • Fatal Failure | Критический отказ — проверка не выполнена, выдается диагностическое сообщение, и выполнение теста прерывается.
  • Test | Тест — фрагмент программы с набором утверждений (проверок), которые проверяют состояние программы на разных этапах выполнения.
  • Test Case | Набор тестов — тесты, объединенные разработчиком в некоторую логическую группу, например, все тесты, связанные с каким либо модулем / классом.
  • Fixture | Фиксация — объект, связанный с тестовым набором, и выполняющий инициализацию / деинициализацию тестового окружения, для независимого выполнения каждого теста из набора.

Пройдемся по ним более подробно и с примерами.

Утверждения

Утверждения реализованы в виде макросов (макро-функций).

Утверждения, начинающиеся с префикса ASSERT_, порождают в случае неуспеха критические отказы. Утверждения, начинающиеся с префикса EXPECT_ — некритические.

В случае критического отказа происходит немедленный возврат из функции, в которой проверялось это утверждение. Если за этим утверждением идет какой-то очищающий память код или какие-то другие завершающие процедуры, то можно получить утечку памяти / ресурсов.

Далее приводится синтаксис для критических утверждений, для некритических синтаксис аналогичен, различается только префикс в имени макро-функции (ASSERT_ / EXPECT_).

Простые логические проверки

  • ASSERT_TRUE(выражение); — проверка выражения на истинность
  • ASSERT_FALSE(выражение); — проверка выражения на ложность
std::string s;
ASSERT_TRUE(s.empty()); // строка изначально пустая
s = "Hello, tests";
ASSERT_FALSE(s.empty()); // после инициализации строка не пустая

Проверка значений

Значения сравниваются с помощью операторов сравнения.

  • ASSERT_EQ(ожидаемое, реальное); — проверка, что реальное значение точно равно ожидаемой величине
  • ASSERT_NE(ожидаемое, реальное); — проверка, что реальное значение не равно ожидаемой величине
  • ASSERT_LT(ожидаемое, реальное); — проверка, что ожидаемое значение < реальная величина
  • ASSERT_LE(ожидаемое, реальное); — проверка, что ожидаемое значение <= реальная величина
  • ASSERT_GT(ожидаемое, реальное); — проверка, что ожидаемое значение > реальная величина
  • ASSERT_GE(ожидаемое, реальное); — проверка, что ожидаемое значение >= реальная величина
std::vector<int> v1 = { 1, 2, 3 }, v2 = { 2, 3 };
ASSERT_LE(v1.size(), v1.capacity()); // ёмкость вектора больше либо равна его размеру
ASSERT_LE(v2.size(), v2.capacity()); // ёмкость вектора больше либо равна его размеру
ASSERT_NE(v1, v2); // вектора не должны быть равны

Есть особый случай, который в GoogleTest обрабатывается не очевидным образом — это проверка указателя на равенство / не равенство NULL. Обычное сравнение не вполне корректно работает, так как чаще всего указатель NULL определён как целое число 0, и его тип не совпадает с типом указателя.

int * p = NULL;
ASSERT_EQ(NULL, p); // работает
p = new int[100];
ASSERT_NE(NULL, p); // не компилируется
delete[] p;

Разработчики предлагают следующие обходные варианты решения:

int * p = NULL;
ASSERT_TRUE(p == NULL);
p = new int[100];
ASSERT_FALSE(p == NULL); // или ASSERT_TRUE(p != NULL);
delete[] p;
int * p = nullptr;
ASSERT_EQ(nullptr, p); // работает, C++11
p = new int[100];
ASSERT_NE(nullptr, p); // работает, C++11
delete[] p;

Сравнение строк

  • ASSERT_STREQ(ожидаемая, реальная); — проверка C-строк на равенство
  • ASSERT_STRNE(ожидаемая, реальная); — проверка C-строк на неравенство
  • ASSERT_STRCASEEQ(ожидаемая, реальная); — регистронезависимая проверка C-строк на равенство
  • ASSERT_STRCASENE(ожидаемая, реальная); — регистронезависимая проверка C-строк на неравенство
std::string s1 = "Hello", s2 = "world";
ASSERT_STRCASEEQ("Hello World!", (s1 + " " + s2 + '!').c_str()); // регистронезависимое сравнение C-строк
ASSERT_EQ("Hello world!", s1 + " " + s2 + '!'); // сравнение std::string объектов

Сравнение чисел с плавающей запятой

  • ASSERT_FLOAT_EQ(ожидаемое, реальное); — неточная проверка двух float значений на равенство
  • ASSERT_DOUBLE_EQ(ожидаемое, реальное); — неточная проверка двух double значений на равенство
  • ASSERT_NEAR(ожидаемое, реальное, абсолютная_ошибка); — неточная проверка двух значений с плавающей запятой на равенство с заданной величиной допустимой ошибки
ASSERT_FLOAT_EQ(0.33333347f, 1.0f / 3); // проверка вычислений с одинарной точностью
ASSERT_DOUBLE_EQ(0.3333333333333335, 1.0 / 3); // проверка вычислений с двойной точностью
ASSERT_NEAR(4.0, sin(3.14 / 2), 3.5); // проверка sin в военных условиях

Вызов отказа или успеха

  • SUCCEED(); — немедленное успешное завершение теста
  • FAIL(); — немедленное завершение теста с критическим отказом
  • ADD_FAILURE(); — вызвать некритический отказ и продолжить выполнение теста

В случае отказа, выдаётся диагностическое сообщение с данными, использованными в утверждении. Кроме того, можно задать собственный комментарий:

const float war_limit = 4.0;
ASSERT_FLOAT_EQ(war_limit, sin(3.14 / 2)) << "war game is over";
d:\work\test\main.cpp(21): error: Value of: sin(3.14 / 2)
  Actual: 0.9999997
Expected: war_limit
Which is: 4
war game is over

Тесты и наборы тестов

Для объявления и реализации теста используется макро-функция TEST(). Этот макрос указывает набор, в который будет включён тест, и организует возвращающую void функцию, в которой можно использовать утверждения.

TEST(TestCaseName, TestName) {
    // тестовый код и проверочные утверждения
}

Макро-функция TEST() принимает 2 параметра, уникально идентифицирующие тест, — название тестового набора и название теста. В рамках одного и того же тестового набора названия тестов не должны совпадать. Разработчики GoogleTest настоятельно советуют не использовать в названиях тестовых наборов символы подчёркивания, и этому правилу стоит следовать. Если название начинается с префикса DISABLED_, это означает, что тест (набор тестов) помечен как временно не используемый.

Пример теста.

TEST(TestCase1, TestA) {
    ASSERT_FALSE(true == false) << "Это просто праздник какой-то.";
}

Как отмечалось ранее, критический отказ вызывает немедленный возврат из функции. Утверждения можно использовать не только в составе теста, но и вызывать из любой функции. Имеется лишь одно ограничение — утверждения, порождающие критические отказы не могут быть вызваны из функций возвращающих не void.

Фиксации и тестовое окружение

Фиксация представляет собой класс, унаследованный от ::testing::Test, внутри которого объявлены все необходимые для тестирования объекты при этом в функции SetUp() выполняется инициализация тестового окружения перед прогоном очередного теста, а в функции TearDown() — обратная деинициализация после прогона этого теста.

class TestCaseFixing : public ::testing::Test {
protected:
    virtual void SetUp(void) {
        // настройка перед запуском очередного теста
    }
    virtual void TearDown(void) {
        // очистка после прогона очередного теста
    }

    std::string settings; // объекты тестового окружения, доступные в каждом тесте
};

Тесты, в которых используются фиксации, должны быть объявлены с помощью макро-функции TEST_F().

TEST_F(TestCaseFixing, TestName) {
    // тестовый код и проверочные утверждения
    settings = "bla-bla-bla"; // использование объектов тестового окружения
}

Эта макро-функция в использовании полностью аналогична TEST(), только в качестве первого параметра ей передаётся не название набора тестов, а название фиксации.

С помощью фиксаций, например, можно тестировать код на утечки памяти.

#include <crtdbg.h> // Windows only

class TestCase1 : public ::testing::Test {
protected:
    virtual void SetUp(void) {
        _CrtMemCheckpoint(&startup);
    }
    virtual void TearDown(void) {
        _CrtMemState teardown, diff;
        _CrtMemCheckpoint(&teardown);
        ASSERT_EQ(0, _CrtMemDifference(&diff, &setup, &teardown)) << "Memory leaks detected";
    }
    _CrtMemState startup;
};

Запуск тестов

Объявив все необходимые тесты, мы можем запустить их с помощью функции RUN_ALL_TESTS(). Функцию можно вызывать только один раз. Желательно, чтобы тестовая программа возвращала результат работы функции RUN_ALL_TESTS(), так как некоторые автоматические средства тестирования определяют результат выполнения тестовой программы по тому, что она возвращает.

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

Дополнение

Настройки для Qt

INCLUDEPATH = \
    ../gtest/googletest/include \
    ../gtest/googletest

SOURCES += \
    ../gtest/googletest/src/gtest-all.cc \