Google Test(gtest) является одной из популярных библиотек модульного тестирования. Тесты не требуется отдельно регистрировать для запуска. Каждый объявленный в программе тест автоматически будет запущен. Тесты можно объединять в группы (наборы). Кроме того, gtest позволяет использовать тестовые классы/фикстуры (test fixture). С их помощью можно создавать и повторно использовать конфигурацию объектов для нескольких различных тестов.
Пусть нужно протестировать отдельную функцию с сигнатурой int Fibonacci(int n), осуществляющую расчет n-го числа Фибоначчи. Тест для этой функции может выглядеть так:
TEST(FibonacciTest, HandlesZeroInput) {
EXPECT_EQ(0, Fibonacci(0));
}
Макрос TEST — это основной инструмент для определения отдельного тестового случая (кейса). Он позволяет программисту создавать автономные тесты, которые gtest автоматически обнаружит и запустит.
Общий вид макроса:
TEST(TestSuiteName, TestName) {
// Здесь размещается тело теста с проверками (assertions)
}
TestSuiteName (имя набора) задает логическую группировку связанных тестов. В выводе результатов тесты с одинаковым TestSuiteName будут идти вместе.
TestName (имя теста) представляет уникальное название конкретной проверки внутри набора.
Проверки, например, EXPECT_EQ или ASSERT_TRUE называются утверждениями, представляющими собой макросы, аналогичные вызовам функций. Они используются для проверки поведения вашего кода различными способами. Утверждения могут проверять логические условия, сравнивать значения, проверять строковые и числа с плавающей запятой. Если все утверждения, заданные в TEST, выполняются, то тест считается пройденным.
В противном случае будет выведено сообщение об ошибке вместе с сопутствующей информацией, помогающей найти место в тестах, где произошла ошибка. Также можно указать собственное сообщение об ошибке.
В gtest существует два типа утверждений:
- EXPECT_*
- ASSERT_*
Утверждение EXPECT_* является некритическим. Если оно не выполняется, проверка продолжается. Кроме того, вы также можете добавлять пользовательские сообщения об ошибках к утверждениям с помощью оператора <<.
Некоторые из часто используемых утверждений EXPECT_*:
- EXPECT_TRUE(condition) — для проверки истинности условия.
- EXPECT_EQ(val1, val2) — для проверки равенства.
- EXPECT_LT(val1, val2) — проверяет, меньше ли val1, чем val2.
- EXPECT_GT(val1, val2) — проверяет, больше ли val1, чем val2.
- EXPECT_THROW(statement, exception_type) — ожидает выбрасывания определенного исключения.
- EXPECT_STREQ(str1, str2) — проверяет, равны ли две заданные строки.
В отличие от EXPECT_*, утверждения ASSERT_* являются фатальными. В случае ошибки текущая функция теста немедленно прерывается, что также предотвращает выполнение любого дальнейшего кода в этом наборе тестов.
Следует использовать ASSERT_*, если продолжение теста не имеет смысла или может привести к вводящим в заблуждение результатам.
Некоторые из часто используемых утверждений ASSERT_*:
- ASSERT_TRUE(condition) — проверяет, истинно ли условие.
- ASSERT_EQ(val1, val2) — проверяет равенство двух значений.
- ASSERT_LT(val1, val2) — проверяет, меньше ли первое значение второго.
- ASSERT_GT(val1, val2) — проверяет, больше ли первое значение второго.
- ASSERT_THROW(statement, exception_type) — ожидает, что указанный оператор вызовет конкретное исключение.
Кроме TEST также существует макрос TEST_F, (где буква F означает fixture). Он используется, когда вам нужно запустить несколько тестов, работающих с общими данными или с одинаковой настройкой окружения.
В отличие от обычного TEST, который автономен, TEST_F позволяет избежать дублирования кода(принцип DRY), вынося подготовку объектов в отдельный класс.
Для использования TEST_F необходимо выполнить следующие шаги:
- Создание класса-фикстуры. Вы создаете класс, наследующий от ::testing::Test.
- Подготовка (SetUp). В методе SetUp можно инициализировать объекты, открывать файлы или подключаться к базе данных.
- Очистка (TearDown). В методе TearDown() освобождаете ресурсы.
- Использование в тестах. В макросе TEST_F первым аргументом указывается имя этого класса, а вторым - имя теста(TestName).
В качестве примера рассмотрим тестирование двух стеков:
Ключевыми особенностями подхода c использованием класса-фикстуры являются:
- Изоляция. Для каждого отдельного макроса TEST_F gtest создает новый экземпляр класса-фикстуры. Изменения данных в одном тесте никак не повлияют на другой.
- Доступ к полям. Все переменные, объявленные в секции protected или public класса фикстуры, доступны внутри тела теста.
Также gtest позволяет создавать тесты, которые запускаются многократно с разными входными данными, но используют один и тот же программный код (логику проверки). Такие тесты называются параметризованными.
Вместо того чтобы писать множество отдельных тестов, вы пишете один шаблон, а затем передаете ему список параметров. Таки тесты нужны когда необходимо запустить один и тот же тест многократно, но с разными входными данными и ожидаемыми результатами, не дублируя код самого теста.
Перед началом тестирования нужно создать структуру данных, хранящую входные данные и ожидаемый результат, например:
struct MyParams { int input; int expected; };
Далее создаем класс-фикстуру, наследующий от ::testing::TestWithParam<T>, где T — тип ваших данных (MyParams):
class MathTest : public ::testing::TestWithParam<MyParams> {};
Далее пишем тест, где вместо TEST_F используется макрос TEST_P (буква P означает Parametric). Внутри теста вы получаете данные через метод GetParam(), например:
TEST_P(MathTest, SquareCheck) {
auto params = GetParam();
EXPECT_EQ(params.input * params.input, params.expected);
}
После этого необходимо задать набор данных и передать его в тест при помощи макроса INSTANTIATE_TEST_SUITE_P:
INSTANTIATE_TEST_SUITE_P(MyGroup, MathTest, ::testing::Values(
MyParams{2, 4},
MyParams{3, 9},
MyParams{5, 25}
));
В результате gtest автоматически создаст и запустит 3 отдельных теста, подставляя каждое значение из набора.
Макрос INSTANTIATE_TEST_SUITE_P связывает параметризованный тест, написанный через TEST_P, с конкретным набором данных. У него три основных обязательных параметра и один необязательный для именования:
INSTANTIATE_TEST_SUITE_P(
InstantiationName,
TestSuiteName,
ParamGenerator,
NameGenerator // (опционально)
);
InstantiationName (имя экземпляра) задает уникальное имя для данной группы тестов.
TestSuiteName (имя тестового набора). Должно в точности совпадать с именем класса-фикстуры, который наследует от ::testing::TestWithParam<T>.
ParamGenerator (Генератор параметров):
Самая важная часть, определяющая, какие данные будут переданы в тест. gtest предоставляет встроенные вспомогательные функции (хелперы):
- Values(v1, v2, ...) — перечисление конкретных значений.
- ValuesIn(container) — берет данные из массива или вектора.
- Range(begin, end, step) — генерирует последовательность чисел.
- Bool() — генерирует true и false.
- Combine(g1, g2, ...) — создает декартово произведение (все возможные комбинации нескольких наборов).
NameGenerator (необязательный параметр). Функция или лямбда, которая превращает текущий параметр в строку. По умолчанию gtest просто нумерует запуски (0, 1, 2), но с помощью этого параметра в отчете можно увидеть более содержательные названия.
Использование параметризованных тестов позволяет достигать следующих целей:
- Чистота кода. Не нужно писать множество похожих тестов для разных входных данных.
- Информативность. Если тест упадет на одном конкретном значении (например, на числе 0), gtest покажет, какой именно набор параметров вызвал ошибку, при этом остальные тесты продолжат выполняться.
- Гибкость. Можно генерировать параметры динамически (через диапазоны Range, списки ValuesIn или комбинации Combine).
- Изоляция. Несмотря на то что код один, каждый запуск с новым параметром считается отдельным тестом.
На этом закончим описание типов тестов. В следующей статье рассмотрим тестирование вызовов.