Тестування з CMake: CTest, Google Test, покриття коду
Тестування з CMake: CTest, Google Test, покриття коду
🎯 Мета лекції
- Підключити тести до проєкту через
enable_testing()таadd_test(). - Запускати тести утилітою
ctest, фільтрувати їх і читати результати. - Інтегрувати Google Test та автоматично реєструвати його тести в CTest.
- Розуміти, чому
assertу Release-збірці не перевіряє нічого. - Виміряти покриття коду тестами.
🔑 Ключові терміни
- Юніт-тест (unit test): невелика програма, яка перевіряє окрему функцію або клас.
- CTest: утиліта запуску тестів, що входить до складу CMake.
- Тестовий фреймворк: бібліотека з макросами перевірок і запуском тестів (Google Test, Catch2).
- Покриття коду (code coverage): відсоток рядків програми, які виконались під час тестів.
Збирати програму недостатньо: потрібно переконатися, що вона працює правильно. Автоматичні тести виконуються після кожної зміни і швидко показують, що саме зламалось. CMake має вбудований інструмент для їх запуску.
CTest: вбудована система тестування
CTest — утиліта, що йде разом із CMake. Її задача — запускати програми, зареєстровані як тести, і збирати результат. Тест вважається успішним, якщо програма завершилась із кодом 0, і провальним в усіх інших випадках (ненульовий код, аварійне завершення, перевищення часу).
Структура проєкту
Ключові рядки:
enable_testing()вмикає тестову інфраструктуру. Її потрібно викликати в кореневомуCMakeLists.txtдоadd_subdirectory(tests), інакше CTest не знайде тести з підпапок.add_test(NAME ... COMMAND ...)реєструє тест: ім'я для звітів і команду запуску. Як команда можна вказати ім'я ціліtest_math, і CMake сам підставить шлях до виконуваного файлу.target_link_libraries(test_math MathUtils)підключає тестовану бібліотеку. ЗавдякиPUBLICуMathUtilsтест отримує і шлях до заголовків.
Запуск тестів
# Збираємо проєкт
cmake -S . -B build
cmake --build build
# Запускаємо тести з папки збірки
cd build
ctest
# Докладний вивід
ctest --verbose
# Показувати вивід лише для провальних тестів
ctest --output-on-failure
Корисні прапорці CTest
| Команда | Дія |
|---|---|
ctest -R Math | Запустити тести, імена яких відповідають регулярному виразу Math |
ctest -E Slow | Виключити тести за іменем |
ctest -j 4 | Запускати до 4 тестів паралельно |
ctest -C Debug | Вказати конфігурацію для multi-config генераторів (Visual Studio, Xcode) |
ctest --rerun-failed | Повторити лише ті тести, що впали минулого разу |
ctest --timeout 10 | Обмежити час кожного тесту десятьма секундами |
Для окремого тесту можна задати властивості:
add_test(NAME MathTests COMMAND test_math)
set_tests_properties(MathTests PROPERTIES TIMEOUT 5)
Пастка: assert у Release
Спробуємо зламати перевірку: замінимо в тесті assert(add(2, 3) == 5) на assert(add(2, 3) == 6). Це свідомо хибне твердження. Зберемо проєкт у двох конфігураціях і запустимо CTest:
У Debug CTest виявив помилку. У Release той самий хибний тест «пройшов». Причина: Release-збірка визначає макрос NDEBUG, а assert при ньому розгортається в порожнє місце, і перевірок у виконуваному файлі просто немає.
assert у тестах. Використовуйте тестовий фреймворк, макроси якого не залежать від NDEBUG, або власну перевірку з поверненням ненульового коду.Google Test
Для серйозного тестування потрібен фреймворк: він надає макроси порівняння, групує тести, виводить зрозумілі повідомлення про розбіжності та підтримує параметризовані тести. Найпоширеніший варіант — Google Test.
Підключення через FetchContent
cmake_minimum_required(VERSION 3.14)
project(GoogleTestExample)
set(CMAKE_CXX_STANDARD 17)
# Модуль для завантаження залежностей
include(FetchContent)
# Завантажуємо Google Test
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
# Для Windows: не змінювати налаштування CRT батьківського проєкту
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# Бібліотека, яку тестуємо
add_library(MathUtils src/math_utils.cpp)
target_include_directories(MathUtils PUBLIC src)
# Вмикаємо тестування
enable_testing()
# Тестовий виконуваний файл
add_executable(math_tests tests/test_math.cpp)
# Лінкуємо з Google Test і з нашою бібліотекою
target_link_libraries(math_tests
GTest::gtest_main
MathUtils
)
# Автоматично знаходимо тести
include(GoogleTest)
gtest_discover_tests(math_tests)
Розбір нових елементів:
GTest::gtest_main— ціль Google Test, яка містить функціюmain(), що запускає всі тести. У тестовому файлі писатиmainне потрібно.gtest_force_shared_crtпотрібна лише на Windows з MSVC: вона змушує Google Test використовувати ту саму бібліотеку часу виконання, що й проєкт, і запобігає помилкам лінкування.gtest_discover_testsзапускає тестовий файл із прапорцем--gtest_list_tests, отримує список тестів і реєструє кожен як окремий тест CTest. Одногоadd_testна весь файл вистачає лише для простих випадків.
Тести на Google Test
#include <gtest/gtest.h>
#include "math_utils.h"
// Тест функції add
TEST(MathUtilsTest, Add) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-1, 1), 0);
EXPECT_EQ(add(0, 0), 0);
EXPECT_EQ(add(100, -50), 50);
}
// Тест функції multiply
TEST(MathUtilsTest, Multiply) {
EXPECT_EQ(multiply(2, 3), 6);
EXPECT_EQ(multiply(-2, 3), -6);
EXPECT_EQ(multiply(0, 100), 0);
EXPECT_EQ(multiply(7, 8), 56);
}
// Тест функції isPrime
TEST(MathUtilsTest, IsPrime) {
EXPECT_TRUE(isPrime(2));
EXPECT_TRUE(isPrime(3));
EXPECT_TRUE(isPrime(5));
EXPECT_TRUE(isPrime(17));
EXPECT_TRUE(isPrime(97));
EXPECT_FALSE(isPrime(0));
EXPECT_FALSE(isPrime(1));
EXPECT_FALSE(isPrime(4));
EXPECT_FALSE(isPrime(20));
EXPECT_FALSE(isPrime(100));
}
// Параметризований тест: один тест, багато значень
class PrimeParameterizedTest : public testing::TestWithParam<int> {};
TEST_P(PrimeParameterizedTest, KnownPrimes) {
EXPECT_TRUE(isPrime(GetParam()));
}
INSTANTIATE_TEST_SUITE_P(
KnownPrimes,
PrimeParameterizedTest,
testing::Values(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
);
Макрос TEST(Набір, Ім'я) оголошує тест, а EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE перевіряють результати. Відмінність між EXPECT_* і ASSERT_*: після невдалого EXPECT_* тест продовжується (видно всі помилки одразу), а ASSERT_* зупиняє тест на першій же невдачі. Параметризований тест TEST_P запускає ту саму перевірку для кожного значення зі списку.
Запуск
cmake -S . -B build
cmake --build build
cd build
ctest --verbose
# Запустити виконуваний файл напряму
./math_tests
# Один конкретний тест
./math_tests --gtest_filter=MathUtilsTest.Add
# Показати список тестів без виконання
./math_tests --gtest_list_tests
Завдяки gtest_discover_tests CTest бачить кожен тест окремо (три прості плюс десять параметризованих):
release-1.12.1 використана для відтворюваності прикладу. У нових проєктах зазвичай беруть найновіший стабільний реліз Google Test. Конкретний тег варто перевіряти на сторінці релізів.Альтернатива: Google Test через vcpkg
Якщо у проєкті вже використовується vcpkg (його розібрано в наступних лекціях), Google Test не потрібно завантажувати при кожній конфігурації:
find_package(GTest CONFIG REQUIRED)
add_executable(math_tests tests/test_math.cpp)
target_link_libraries(math_tests PRIVATE GTest::gtest_main MathUtils)
include(GoogleTest)
gtest_discover_tests(math_tests)
Решта CMake-коду залишається тим самим: змінюється лише спосіб отримання залежності.
Покриття коду
Покриття показує, які рядки програми виконались під час тестів. Рядки з нульовим покриттям — це код, який жоден тест не перевіряє.
cmake_minimum_required(VERSION 3.10)
project(CoverageExample)
set(CMAKE_CXX_STANDARD 17)
# Опція для покриття
option(ENABLE_COVERAGE "Enable code coverage" OFF)
if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
message(STATUS "Code coverage enabled")
# Прапорці для збору статистики
add_compile_options(--coverage -O0 -g)
add_link_options(--coverage)
endif()
add_library(MathUtils src/math_utils.cpp)
target_include_directories(MathUtils PUBLIC src)
enable_testing()
add_executable(math_tests tests/test_math.cpp)
target_link_libraries(math_tests GTest::gtest_main MathUtils)
gtest_discover_tests(math_tests)
Прапорець --coverage змушує компілятор вставити в програму лічильники, що рахують, скільки разів виконано кожен рядок. Оптимізація (-O0) вимкнена, щоб рядки відповідали вихідному коду. Після запуску тестів статистика записується у файли .gcda.
# Збираємо з покриттям
cmake -S . -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# Запускаємо тести
ctest --test-dir build
# Генеруємо звіт (потрібна утиліта gcovr або lcov)
gcovr -r . --html --html-details -o coverage.html
# Відкриваємо звіт у браузері
Прапорець --test-dir build дозволяє запустити CTest без cd build (CMake 3.20 і новіше). Звіт coverage.html показує відсоток покриття для кожного файлу і підсвічує рядки, які не виконувались.
Питання для самоперевірки
0. Будь-який інший код повернення, аварійне завершення (як у випадку assert) або перевищення часу вважаються провалом.enable_testing() викликано лише в підпапці, у корені немає файлу CTestTestfile.cmake, і ctest у папці збірки нічого не знайде.add_test весь тестовий виконуваний файл — один тест CTest: якщо падає одна перевірка, видно лише загальний провал. gtest_discover_tests реєструє кожен тест Google Test окремо, тому CTest показує, який саме впав, і дозволяє запускати їх по одному (ctest -R Add) або паралельно.NDEBUG, і assert вилучається препроцесором. Тест пройде навіть із хибним твердженням. Тому перевірки, що мають працювати у всіх конфігураціях, пишуть у фреймворку (EXPECT_EQ) або в явних if з поверненням коду помилки.Практичне завдання
- Візьміть бібліотеку
StringUtilsз лекції про бібліотеки (toUpperCase,toLowerCase,split). - Підключіть Google Test через
FetchContent. - Напишіть мінімум по два тести на кожну функцію, включно з порожнім рядком.
- Для
splitдодайте параметризований тест для роздільників,,;та пробіл. - Переконайтеся, що
ctestпоказує кожен тест окремо, а потім навмисно зламайте одну перевірку й подивіться на повідомлення.
#include <gtest/gtest.h>
#include "mylib/string_utils.h"
TEST(StringUtilsTest, UpperCase) {
EXPECT_EQ(StringUtils::toUpperCase("hello"), "HELLO");
EXPECT_EQ(StringUtils::toUpperCase(""), "");
}
TEST(StringUtilsTest, SplitCsv) {
auto parts = StringUtils::split("a,b,c", ',');
ASSERT_EQ(parts.size(), 3u);
EXPECT_EQ(parts[0], "a");
EXPECT_EQ(parts[2], "c");
}
Використання ASSERT_EQ для розміру захищає від звернення за межі вектора в наступних рядках.
Резюме
🧪 CTest
enable_testing() у корені і add_test() реєструють тести. ctest запускає їх, фільтрує та показує результат.✅ Google Test
FetchContent або vcpkg. gtest_discover_tests перетворює кожен тест на окремий запис CTest.⚠️ assert і NDEBUG
assert зникає, і хибний тест проходить. Для перевірок використовуйте фреймворк.📊 Покриття
--coverage збирає статистику виконаних рядків, gcovr будує звіт. Корисне доповнення, а не самоціль..gitignore варто тримати під рукою, а також скрипти збірки й перелік кращих практик сучасного CMake.Структура великого проєкту: підпапки, модулі CMake, PCH та install
Рекомендована структура папок C++ проєкту, ієрархія CMakeLists.txt, внутрішні бібліотеки, власні модулі .cmake для попереджень компілятора, попередньо скомпільовані заголовки, команда install.
Команди CMake, пресети, скрипти збірки та кращі практики
Довідник команд cmake, корисні змінні, .gitignore для CMake-проєктів, скрипти збірки для Linux/macOS і Windows, CMakePresets.json, compile_commands.json та шість правил сучасного CMake.