Поиск по сайту:

Выполняйте модульные тесты с помощью 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, который дает вам обзор идей, возможностей и особенностей фреймворка.

Статьи по данной тематике: