Выполняйте модульные тесты с помощью GoogleTest и CTest.
Использование модульных тестов, скорее всего, улучшит качество вашего кода, не нарушая при этом рабочий процесс.
Эта статья является продолжением моей предыдущей статьи «Настройка системы сборки с помощью CMake и VSCodium».
В прошлой статье я показал, как настроить систему сборки на базе VSCodium и CMake. В этой статье эта настройка уточняется путем интеграции содержательных модульных тестов с помощью GoogleTest и CTest.
Если это еще не сделано, клонируйте репозиторий, откройте его в VSCodium и извлеките тег devops_2, щелкнув символ main-ветви (красный маркер) и выбрав ветку (желтый маркер):
Стефан Авенведде (CC BY-SA 4.0)
Альтернативно откройте командную строку и введите:
$ git checkout tags/devops_2
GoogleТест
GoogleTest — это независимая от платформы среда тестирования C++ с открытым исходным кодом. Хотя GoogleTest не предназначен исключительно для модульных тестов, я буду использовать его для определения модульных тестов для библиотеки Generator. В общем, модульный тест должен проверять поведение одного логического модуля. Библиотека Generator представляет собой единое целое, поэтому я напишу несколько содержательных тестов, чтобы гарантировать правильную работу.
При использовании GoogleTest тестовые случаи определяются макросами утверждений. Обработка утверждения генерирует один из следующих результатов:
- Успех: тест пройден.
- Нефатальный сбой: тест не пройден, но функция тестирования продолжится.
- Неустранимая ошибка: тест не пройден, и функция тестирования будет прервана.
Макросы утверждений следуют этой схеме, чтобы отличить фатальный сбой от нефатального:
ASSERT_*
фатальный сбой, функция прерывается.EXPECT_*
нефатальный сбой, функция не прерывается.
Google рекомендует использовать макросы EXPECT_*
, поскольку они позволяют продолжить тест, если тесты определяют несколько утверждений. Макрос утверждения принимает два аргумента: первый аргумент — это имя группы тестов (свободно выбираемая строка), а второй аргумент — имя самого теста. Библиотека Generator просто определяет функцию generate(...), поэтому тесты в этой статье относятся к одной группе: GeneratorTest.
Следующие модульные тесты для функции generate(...) можно найти в GeneratorTest.cpp.
Проверка рекомендаций
Функция генерации(...) принимает ссылку на std::stringstream в качестве аргумента и возвращает ту же ссылку. Итак, первый тест — проверить, является ли переданная ссылка той же ссылкой, которую возвращает функция.
TEST(GeneratorTest, ReferenceCheck){
const int NumberOfElements = 10;
std::stringstream buffer;
EXPECT_EQ(
std::addressof(buffer),
std::addressof(Generator::generate(buffer, NumberOfElements))
);
}
Здесь я использую std::addressof, чтобы проверить, относится ли адрес возвращаемого объекта к тому же объекту, который я указал в качестве входных данных.
Количество элементов
Этот тест проверяет, соответствует ли количество элементов в ссылке на строковый поток числу, указанному в качестве аргумента.
TEST(GeneratorTest, NumberOfElements){
const int NumberOfElements = 50;
int nCalcNoElements = 0;
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::string s_no;
while(std::getline(buffer, s_no, ' ')) {
nCalcNoElements++;
}
EXPECT_EQ(nCalcNoElements, NumberOfElements);
}
Перетасовать
Этот тест проверяет правильность работы механизма случайных чисел. Если я вызову функцию generate два раза подряд, я ожидаю, что не получу одинаковый результат.
TEST(GeneratorTest, Shuffle){
const int NumberOfElements = 50;
std::stringstream buffer_A;
std::stringstream buffer_B;
Generator::generate(buffer_A, NumberOfElements);
Generator::generate(buffer_B, NumberOfElements);
EXPECT_NE(buffer_A.str(), buffer_B.str());
}
Контрольная сумма
Это самое масштабное испытание. Он проверяет, совпадает ли сумма цифр числового ряда от 1 до n с суммой перетасованного выходного ряда. Я ожидаю, что сумма совпадает, поскольку функция generate(...) должна просто создавать перетасованный вариант такой серии.
TEST(GeneratorTest, CheckSum){
const int NumberOfElements = 50;
int nChecksum_in = 0;
int nChecksum_out = 0;
std::vector<int> vNumbersRef(NumberOfElements); // Input vector
std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector
// Calculate reference checksum
for(const int n : vNumbersRef){
nChecksum_in += n;
}
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::vector<int> vNumbersGen; // Output vector
std::string s_no;
// Read the buffer back back to the output vector
while(std::getline(buffer, s_no, ' ')) {
vNumbersGen.push_back(std::stoi(s_no));
}
// Calculate output checksum
for(const int n : vNumbersGen){
nChecksum_out += n;
}
EXPECT_EQ(nChecksum_in, nChecksum_out);
}
Вышеупомянутые тесты также можно отлаживать, как обычное приложение C++.
CTest
В дополнение к модульному тесту в коде утилита CTest позволяет мне определять тесты, которые можно выполнять для исполняемых файлов. Короче говоря, я вызываю исполняемый файл с определенными аргументами и сопоставляю выходные данные с регулярными выражениями. Это позволяет мне просто проверить, как ведет себя исполняемый файл с неправильными аргументами командной строки. Тесты определены в файле CMakeLists.txt верхнего уровня. Вот более детальный взгляд на три тестовых случая:
Регулярное использование
Если в качестве аргумента командной строки указано положительное целое число, я ожидаю, что исполняемый файл выдаст серию чисел, разделенных пробелами:
add_test(NAME RegularUsage COMMAND Producer 10)
set_tests_properties(RegularUsage
PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+"
)
Нет аргументов
Если аргумент не указан, программа должна немедленно завершить работу и отобразить причину:
add_test(NAME NoArg COMMAND Producer)
set_tests_properties(NoArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument"
)
Неправильный аргумент
Предоставление аргумента, который невозможно преобразовать в целое число, также должно вызывать немедленный выход с сообщением об ошибке. Этот тест вызывает исполняемый файл Producer с параметром командной строки"ABC":
add_test(NAME WrongArg COMMAND Producer ABC)
set_tests_properties(WrongArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse"
)
Тестирование тестов
Чтобы запустить один тест и посмотреть, как он обрабатывается, вызовите ctest
из командной строки, указав следующие аргументы:
- Запустите одиночный тест:
-R <имя-теста>
- Включить подробный вывод:
-VV
Вот команда ctest -R Использование -VV:
$ ctest -R Usage -VV
UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
Test project /home/stephan/Documents/cpp_testing sample/build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
В этом блоке кода я вызвал тест под названием Usage.
Это запустило исполняемый файл без аргументов командной строки:
test 3
Start 3: Usage
3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer
Тест не прошёл, поскольку выходные данные не соответствуют регулярному выражению [^[0-9]+]
.
3: Enter the number of elements as argument
1/1 test #3. Usage ................
Failed Required regular expression not found.
Regex=[^[0-9]+]
0.00 sec round.
0% tests passed, 1 tests failed out of 1
Total Test time (real) =
0.00 sec
The following tests FAILED:
3 - Usage (Failed)
Errors while running CTest
$
Чтобы запустить все тесты (включая тест, определенный с помощью GoogleTest), перейдите в каталог build и запустите ctest
:
Стефан Авенведде (CC BY-SA 4.0)
Внутри VSCodium щелкните область, отмеченную желтым цветом на информационной панели, чтобы вызвать CTest. Если все тесты пройдены, отображается следующий вывод:
Стефан Авенведде (CC BY-SA 4.0)
Автоматизируйте тестирование с помощью Git Hooks
На данный момент запуск тестов является дополнительным шагом для разработчика. Разработчик также может зафиксировать и отправить код, который не прошел тесты. Благодаря Git Hooks я могу реализовать механизм, который автоматически запускает тесты и предотвращает случайное внесение разработчиком ошибочного кода.
Перейдите к .git/hooks
, создайте пустой файл с именем pre-commit, скопируйте и вставьте следующий код:
#!/usr/bin/sh
(cd build; ctest --output-on-failure -j6)
После этого сделайте этот файл исполняемым:
$ chmod +x pre-commit
Этот сценарий вызывает CTest при попытке выполнить фиксацию. Если тест не пройден, как показано на скриншоте ниже, фиксация прерывается:
Стефан Авенведде (CC BY-SA 4.0)
Если тесты пройдены успешно, фиксация обрабатывается, и выходные данные выглядят следующим образом:
Стефан Авенведде (CC BY-SA 4.0)
Описанный механизм является лишь мягким барьером: разработчик все равно может зафиксировать ошибочный код, используя git commit --no-verify
. Я могу гарантировать, что будет загружен только рабочий код, настроив сервер сборки. Эта тема будет частью отдельной статьи.
Краткое содержание
Методы, упомянутые в этой статье, просты в реализации и помогают быстро находить ошибки в коде. Использование модульных тестов, скорее всего, улучшит качество вашего кода и, как я показал, сделает это, не нарушая рабочий процесс. Платформа GoogleTest предоставляет функции для каждого мыслимого сценария; Я использовал только часть его функций. Здесь я также хочу упомянуть GoogleTest Primer, который дает вам обзор идей, возможностей и особенностей фреймворка.