Предыдущие посты в этой серии постов:

Я работаю в операционной системе Windows 10. У меня установлена среда Visual Studio Community с рабочей нагрузкой «Desktop Development with C++». Но в рамках этой статьи я не использую графический интерфейс среды Visual Studio Community, а работаю из командной строки разработчика этой среды. (Обычная командная строка не подойдет, так как в ней без дополнительной настройки недоступны нужные инструменты.)

Такой способ работы выбран для того, чтобы студенты, использующие операционные системы из семейства Linux (у нас в колледже есть такие), могли выполнить те же операции, что и студенты, использующие системы Windows.

По этой же причине в качестве тестирующего фреймворка выбран Google Test в связке с системой сборки CMake. Оба эти инструмента кроссплатформенные, то есть работают в системах Windows и Linux, а кроме этого они входят в рабочую нагрузку «Desktop Development with C++» среды Visual Studio Community, что удобно для программистов, использующих Windows: нет необходимости в дополнительной установке этих двух инструментов.

Создание подпапок и файлов проекта

Папке проекта я дал название ProjectFolderWithTests. (Название папки рабочего [не учебного] проекта, конечно, должно быть более осмысленным.) Папки и файлы, перечисленные здесь, создаю вручную.

ProjectFolderWithTests
│   CMakeLists.txt
│   main.cpp
│
├───modules
│       CMakeLists.txt
│       lib.cpp
│       lib.h
│
└───tests
        CMakeLists.txt
        tests-for-lib.cpp

Пока что все перечисленные файлы пустые. Для заполнения открываю все файлы одновременно в редакторе кода (мы используем VS Code). Поскольку все эти файлы работают в связке, имеет смысл писать их одновременно.

Файлы я делю на три группы:

  • файлы, выполняющие задачу, для решения которой создан проект: main.cpp, lib.cpp, lib.h;
  • файлы с тестами: пока только один tests-for-lib.cpp;
  • файлы со скриптом для сборки: три файла CMakeLists.txt, каждый в своей папке.

Для сборки у нас будет две цели (target): получение исполняемого файла из файлов первой группы (решение задачи проекта), получение исполняемого файла для тестирования модулей проекта.

Первая цель: исполняемый файл, решающий задачу

Заполняем исходные файлы:

// main.cpp (в кодировке UTF-8)

#include <iostream>
#include "modules/lib.h"

int main()
{
    std::cout << "Факториал числа 8 равен " << factorial(8) << '\n';

    return 0;
}
// modules/lib.h

long long factorial(int n);
// modules/lib.cpp

long long factorial(int n)
{
    if (n < 0) return -1;
    long long fact{ 1 };
    for (int i{ 1 }; i <= n; i++)
    {
        fact *= i;
    }
    return fact;
}

Заполняем файлы для сборки:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.23)
project(ProjectFolderWithTests)

add_subdirectory(modules)

add_executable(app main.cpp)
target_link_libraries(app lib)
# modules/CMakeLists.txt

project(lib)

add_library(lib lib.cpp)

Создаем генератор в дополнительной подпапке build с помощью следующей команды из командной строки:

cmake -B build

Создаем с помощью генератора из подпапки build исполняемый файл приложения с помощью следующей команды из командной строки:

cmake --build build --target app

После этого приложение (оно в данном случае консольное) я запускаю из папки проекта из командной строки так:

build\Debug\app

Вообще исполняемый файл у меня получился app.exe, но расширение можно при запуске из командной строки не указывать. Предварительно я включил для командной строки кодовую страницу 65001 (кодировка UTF-8), чтобы русские буквы отобразились корректно. Это можно сделать с помощью команды chcp 65001. По умолчанию используется конфигурация сборки Debug (в случае наличия нескольких конфигураций сборки можно выбрать конкретную с помощью параметра --config [подробнее] системы сборки CMake).

Вторая цель: тестирование модулей

Заполняем исходные файлы с тестами, используя инструменты, предоставляемые тестовым фреймворком Google Test. (Чтобы получить понимание этих инструментов хотя бы на начальном уровне, рекомендую прочитать учебник для начинающих [primer] на официальном сайте этого фреймворка.) Пока что у меня один исходный файл с тестами:

// tests/tests-for-lib.cpp

#include <gtest/gtest.h>    // заголовочный файл фреймворка
#include "../modules/lib.h" // заголовочный файл тестируемого модуля

TEST(FactorialTestSuite, NegativeInputs)
{
    EXPECT_EQ(-1, factorial(-5));
}

В этом файле пока только один тест, созданный с помощью макроса TEST фреймворка. В будущем можно добавить сколько угодно других тестов. Макрос TEST принимает в скобках название набора тестов (test suite) и название конкретного теста из этого набора. В данном случае набор тестов называется FactorialTestSuite (набор тестов для тестирования модуля/функции, вычисляющей факториал числа), а конкретный тест из этого набора называется NegativeInputs (тестируем работу указанной функции в случае, когда входящий параметр функции является отрицательным числом).

Внутри теста могут содержаться утверждения (assertion). В актуальной версии фреймворка Google Test названия утверждений начинаются либо с последовательности ASSERT_, либо с последовательности EXPECT_. Отличие между ними в следующем. Если тестируемый код не соответствует утверждению ASSERT_, тестирование прерывается. Если тестируемый код не соответствует утверждению EXPECT_, тестирование не прерывается. В данном случае у меня только одно утверждение, поэтому разницы не будет видно. Эта разница имеет значение, когда есть ряд утверждений. Оба варианта имеют свою область применения, а также могут применяться совместно. В учебнике для начинающих есть хороший пример их совместного применения.

Редактор Visual Studio Code, который мы используем, по умолчанию не находит заголовочный файл фреймворка gtest/gtest.h и подчеркивает эту строку красной линией, сигнализируя об ошибке в коде. Я пока не стал разбираться, как с этим справиться, поэтому просто игнорирую это подчеркивание. Тестирование я запускаю из командной строки разработчика Visual Studio, там эта ошибка не возникает.

Дополним главный файл с настройками сборки для системы CMake:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.23)
project(ProjectFolderWithTests)

include(FetchContent)
string(CONCAT GTEST_URL "https://github.com/google/googletest/archive/"
                        "52eb8108c5bdec04579160ae17225d66034bd723.zip")
FetchContent_Declare(
  googletest
  URL ${GTEST_URL}
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

add_subdirectory(modules)
add_subdirectory(tests)

add_executable(app main.cpp)
target_link_libraries(app lib)

По сравнению с предыдущей версией этого файла в этой статье добавился блок кода, начинающийся с инструкции include(FetchContent) и заканчивающийся инструкцией FetchContent_MakeAvailable(googletest). Таким образом мы загружаем из интернета и подключаем к нашему проекту тестирующий фреймворк Google Test. Этот код взят с официального сайта этого фреймворка, из руководства «Quickstart: CMake».

Я добавил в этот код инструкцию, начинающуюся с string(CONCAT GTEST_URL, чтобы уложить длинный URL в традиционную ширину программ (около 80 символов). Для этого есть ряд способов, я выбрал этот, его советуют на сайте Stack Overflow.

Еще я поменял уникальный идентификатор коммита (commit hash) на более актуальный, для самой свежей версии фреймворка Google Test. На данный момент это версия 1.17.0, ей соответствует коммит 52eb8108c5bdec04579160ae17225d66034bd723.

Фреймворк Google Test, встроенный в Visual Studio, у меня не получилось использовать при тестировании из командной строки разработчика Visual Studio. Думаю, встроенный в Visual Studio фреймворк Google Test предназначен только для работы из графического (оконного) интерфейса Visual Studio. Поэтому пришлось использовать вышеописанный способ, использующий модуль FetchContent (подробнее) системы сборки CMake.

Также я добавил в файл CMakeLists.txt инструкцию add_subdirectory(tests) для подключения файла tests/CMakeLists.txt. Заполним этот файл:

# tests/CMakeLists.txt

project(tests)

add_executable(tests tests-for-lib.cpp)
target_link_libraries(tests
    PUBLIC
        lib
        gtest
        gtest_main
)

Вообще в программе с тестами тоже должна быть своя функция main, как и в любой программе на языках Си или C++. Однако, насколько я понимаю, для удобства работы с фреймворком Google Test ее убрали в библиотеку gtest_main, поэтому мы подключаем эту библиотеку, а саму функцию main самостоятельно не пишем.

Генератор был создан ранее, поэтому для компиляции (сборки) тестов использую следующую команду в командной строке (при указании цели вместо app указываю tests):

cmake --build build --target tests

В выданной в консоль информации можно будет увидеть путь к исполняемому файлу. Запускаем этот исполняемый файл из командной строки (у меня он называется tests.exe, но расширение при запуске указывать необязательно):

build\tests\Debug\tests

Результат:

Как видно на вышеприведенной иллюстрации, функция (модуль) factorial успешно прошла наш единственный на данный момент тест.

Полезные ссылки

При подготовке этого поста использовал следующие источники: