Модульные тесты защищают от регрессивных изменений кода и предоставляют разработчикам ПО подробную обратную связь. 

Изучив материал статьи, вы убедитесь, насколько просто добавлять модульные тесты в C/C++ проект с помощью google test.

Начальный этап 

Возьмем простой пример вычисления среднего значения из массива целых чисел. 

calculate_mean принимает на вход массив целых чисел и его длину, а на выходе возвращает среднее значение массива (сумму массива, разделенную на его длину) в виде числа с плавающей точкой (float).

Структура файла 

+ Root
+ modules
- calculations.c
- calculations.h
- CMakeLists.txt
+ tests
- test_calculations.cpp
- CMakeLists.txt
- mainapp.c
- CMakeLists.txt
- conanfile.txt

Примечание. Полный вариант кода данной демоверсии доступен на GitHub

modules/calculations.c

Этот модуль отвечает за вычисления: 

#include <stdio.h>
#include "calculations.h"

int calculate_sum(int arr[], size_t length)
{
int sum = 0;
int i = 0;
for (i = 0; i < length; i++)
{
sum += arr[i];
}

return sum;
}

float calculate_mean(int arr[], size_t length)
{
if(length == 0)
{
// при длине равной 0, мы не можем вычислить среднее значение из-за ошибки деления на 0.
return 0;
}

// в ином случае:
int sum = calculate_sum(arr, length);
return (float)sum / length;
}

Все очень просто. У модуля есть функция для вычисления суммы чисел, которая вызывается для функции calculate_mean, после чего возвращается результат деления суммы на длину. 

Воспользуемся данным кодом в mainapp.c:

#include <stdio.h>
#include "modules/calculations.h"

int main(int argc, char *argv[])
{
int arr[] = {1,5,4,6,7,9,8,10,2};
size_t n = sizeof(arr)/sizeof(arr[0]);
float mean = calculate_mean(arr, n);
printf("Mean=%.2f\n", mean);
return 0;
}

Теперь нам потребуются два файла CMakeLists.txt: один для mainapp, другой для модулей: 

CMakeLists.txt

cmake_minimum_required(VERSION 3.10.2) 
project(MyProject)

add_subdirectory(modules)

add_executable(${PROJECT_NAME} mainapp.c)
target_link_libraries(${PROJECT_NAME} calculations)

modules/CMakeLists.txt

project(calculations) 

add_library(calculations calculations.c calculations.h)

Поясним в двух словах. Модули CMakeLists.txt генерируют библиотеку с именем calculations. Главный CMakeLists.txt использует ее и связывает с главным исполняемым файлом. 

Переходим к этапам компиляции и выполнения. 

Компиляция:

cmake --build ./build --config Debug --target MyProject -j 10 --

Выполнение:

./build/MyProject

Результат:

Mean=5.78

Знакомство с Google Test

Google test или gtest  —  это фреймворк с открытым ПО для модульного тестирования C\C++ проектов. Он легко интегрируется с CMake, располагает превосходным механизмом проверки утверждений и создает отчеты для отображения в формате XML, что позволяет интегрировать его с известными фреймворками CI\CD. 

Шаг 1. Установка gtest из менеджера зависимостей Conan

Создаем файл conanfile.txt в корневом каталоге: 

[requires]

gtest/cci.20210126

[generators]

cmake

Выполняем conan install . -pr=myprofile.

Шаг 2. Добавление gtest в CMakeLists

После установки gtest добавляем его в качестве зависимости в главный файл CMakeLists.txt:

cmake_minimum_required(VERSION 3.10.2)

project(MyProject)

include(${CMAKE_SOURCE_DIR}/conanbuildinfo.cmake)

conan_basic_setup()

add_subdirectory(modules)

add_subdirectory(tests)

add_executable(${PROJECT_NAME} mainapp.c)

target_link_libraries(${PROJECT_NAME} calculations)

Мы дополнили код 4 строками. Одна нужна для включения конфигураций Conan, две  —  для запуска настроек Conan CMake и еще одна  —  для добавления каталога tests

Шаг 3. Написание набора тестов 

tests/test_calculations.cpp

#include <gtest/gtest.h>
extern "C"
{
#include "../modules/calculations.h"
}
TEST(test_calculations, simple_arr)
{
int arr[] = {1, 5, 4, 6, 7, 9, 8, 10, 2, 3};
size_t n = sizeof(arr) / sizeof(arr[0]);
float mean = calculate_mean(arr, n);
EXPECT_FLOAT_EQ(mean, 5.5);
}

TEST(test_calculations, empty_arr)
{
int arr[] = {};
float mean = calculate_mean(arr, 0);
EXPECT_FLOAT_EQ(mean, 0);
}
TEST(test_calculations, all_negatives)
{
int arr[] = {-1, -5, -4, -6, -7, -9, -8, -10, -2, -3};
size_t n = sizeof(arr) / sizeof(arr[0]);
float mean = calculate_mean(arr, n);
EXPECT_FLOAT_EQ(mean, -5.5);
}
TEST(test_calculations, mix_negative_positive)
{
int arr[] = {-1, -5, -4, 6, 7, 9, -8, -10, -2, -3};
size_t n = sizeof(arr) / sizeof(arr[0]);
float mean = calculate_mean(arr, n);
EXPECT_FLOAT_EQ(mean, -1.1);
}
TEST(test_calculations, with_zeros)
{
int arr[] = {-1, -5, -4, 0, 7, 9, 0, -10, -2, -3};
size_t n = sizeof(arr) / sizeof(arr[0]);
float mean = calculate_mean(arr, n);
EXPECT_FLOAT_EQ(mean, -0.89999998);
}

Выше представлен набор тестов, подлежащий выполнению. 

В целом он содержит 5 тест-кейсов, охватывающих разные возможные сценарии. 

Шаг 4. Настройка конфигурации исполняемого файла tests с CMake

tests/CMakeLists.txt

cmake_minimum_required(VERSION 3.10.2)

project(tests)

add_executable(${PROJECT_NAME} test_calculations.cpp)

set(CMAKE_CXX_STANDARD 11)

target_link_libraries(${PROJECT_NAME} PUBLIC

calculations

gtest

gtest_main
)

Здесь даны инструкции по созданию исполняемого файла tests с тремя связанными библиотеками: calculations (модуль, подлежащий тестированию), gtest и gtest_main.

Шаг 5. Выполнение тестов 

Компиляция:

cmake --build ./build --config Debug --target tests -j 10 --

Выполнение:

build\bin\tests.exe

Результат:

[==========] Running 5 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 5 tests from test_calculations
[ RUN      ] test_calculations.simple_arr
[       OK ] test_calculations.simple_arr (0 ms)
[ RUN      ] test_calculations.empty_arr
[       OK ] test_calculations.empty_arr (0 ms)
[ RUN      ] test_calculations.all_negatives
[       OK ] test_calculations.all_negatives (0 ms)
[ RUN      ] test_calculations.mix_negative_positive
[       OK ] test_calculations.mix_negative_positive (0 ms)
[ RUN      ] test_calculations.with_zeros
[       OK ] test_calculations.with_zeros (0 ms)
[----------] 5 tests from test_calculations (70 ms total)

[----------] Global test environment tear-down
[==========] 5 tests from 1 test suite ran. (107 ms total)
[  PASSED  ] 5 tests.

 Все 5 тестов были успешно выполнены и пройдены! 

Запускаем их еще раз и экспортируем результаты в output.xml:

build/bin/tests --gtest_output=xml:output.xml

Полученный результат в формате XML выглядит так:

<?xml version="1.0" encoding="UTF-8"?>

<testsuites tests="5" failures="0" disabled="0" errors="0" time="0.083" timestamp="2022-02-16T12:57:20.151" name="AllTests">

<testsuite name="test_calculations" tests="5" failures="0" disabled="0" skipped="0" errors="0" time="0.055" timestamp="2022-02-16T12:57:20.166">

<testcase name="simple_arr" status="run" result="completed" time="0" timestamp="2022-02-16T12:57:20.171" classname="test_calculations" />

<testcase name="empty_arr" status="run" result="completed" time="0" timestamp="2022-02-16T12:57:20.181" classname="test_calculations" />

<testcase name="all_negatives" status="run" result="completed" time="0" timestamp="2022-02-16T12:57:20.192" classname="test_calculations" />

<testcase name="mix_negative_positive" status="run" result="completed" 
time="0" timestamp="2022-02-16T12:57:20.204" classname="test_calculations" />

<testcase name="with_zeros" status="run" result="completed" time="0" timestamp="2022-02-16T12:57:20.216" classname="test_calculations" />

</testsuite>
</testsuites>

Загружаем его в xUnit Viewer от CodeBeautify

Отлично! 

Такой вариант можно легко опубликовать как результат тестирования в конвейерах ci/cd.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Eldad Uzman: Introduction to Google Test: An Open Source C/C++ Unit-Testing Framework

Предыдущая статьяШесть принципов, которые помогут лучше писать модули для iOS-приложений
Следующая статьяЭффективное итерирование по строкам в Pandas DataFrame