Google Test и Catch2 в C++
Как подключить модульные тесты к C++-проекту на CMake — отдельная программа calc_tests, фреймворки Google Test и Catch2, запуск через ctest. Каркас CMake — CMake — первая программа.
GTest и Catch2 - отдельный test executable
Модульный тест проверяет маленький кусок логики — функцию add, парсер, расчёт НДС — без окна и мыши.
| Ручная проверка | Автотест | |
|---|---|---|
| Когда | "открыл программу, нажал кнопку" | одна команда в терминале / CI |
| Регрессии | легко пропустить | упавший тест сразу виден |
| Скорость | минуты | секунды |
В C++ тесты — второй исполняемый файл: библиотека calc + программа calc_tests со своим main. Так тестовый раннер не смешивается с main приложения.
Обзор фреймворков: Экосистема приложений на C++. Visual Studio + NuGet: Первая программа на C++.
Структура проекта
calc_test/
├── CMakeLists.txt
├── include/calc.hpp
├── src/calc.cpp
└── tests/calc_test.cpp
calc.hpp / calc.cpp — тестируемый код. calc_test.cpp — только проверки.
Google Test + CMake (FetchContent)
CMakeLists.txt:
Код ITЗагрузка примера кода…
Разбор:
add_library(calc ...)выносит тестируемую логику в отдельную цель, чтобы использовать ее и в приложении, и в тестах.FetchContent_Declare/MakeAvailableавтоматически подтягивает Google Test на этапе конфигурации.enable_testing()включает интеграцию сctestна уровне проекта.add_executable(calc_tests ...)создает тестовый раннер как самостоятельный бинарник.target_link_libraries(... GTest::gtest_main)подключает готовую точку входа для запуска всехTEST.
Разбор ключевых строк
| Строка | Смысл |
|---|---|
add_library(calc ...) | логика в библиотеке — и main, и тесты её переиспользуют |
PUBLIC include | у кого линкуется calc, тот видит include/ |
FetchContent_* | скачать Google Test при конфигурации CMake |
enable_testing() | включить поддержку ctest |
GTest::gtest_main | готовый main(), который запускает все TEST(...) |
gtest_discover_tests | зарегистрировать каждый TEST как отдельный тест для ctest |
Файл теста
#include <gtest/gtest.h>
#include "calc.hpp"
TEST(Calc, Adds) {
EXPECT_EQ(5, add(2, 3));
}
Разбор:
#include <gtest/gtest.h>подключает макросы и инфраструктуру Google Test.TEST(Calc, Adds)задает suiteCalcи конкретный сценарийAdds.EXPECT_EQсравнивает ожидаемое и фактическое значение, печатая diff при несовпадении.- Тест проверяет контракт функции
add: сумма2и3должна равняться5.
Разбор макроса TEST(Calc, Adds):
| Часть | Значение |
|---|---|
Calc | имя набора тестов (test suite) |
Adds | имя конкретного теста |
Тело { ... } | что проверяем |
EXPECT_EQ(ожидаемое, фактическое) — при несовпадении тест помечается проваленным, но продолжает выполнение внутри функции.
ASSERT_* — при провале сразу выходит из теста (дальше код бессмысленен).
Сборка и прогон:
cmake -S . -B build && cmake --build build
ctest --test-dir build --output-on-failure
Разбор:
- Первая команда сначала конфигурирует, затем собирает проект вместе с тестовым target-ом.
ctest --test-dir buildзапускает зарегистрированные тесты из папки сборки.--output-on-failureпечатает лог только для упавших кейсов, что ускоряет диагностику.- Такой запуск легко переносится в CI без ручных шагов в IDE.
Catch2 v3
Catch2 нравится синтаксисом TEST_CASE / REQUIRE:
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.4
)
FetchContent_MakeAvailable(Catch2)
add_executable(catch_tests tests/catch_calc.cpp)
target_link_libraries(catch_tests PRIVATE calc Catch2::Catch2WithMain)
Разбор:
- Блок декларативно подключает Catch2 как внешнюю зависимость CMake.
Catch2::Catch2WithMainвключает встроенныйmain, аналогичноgtest_main.catch_testsлинкуется сcalc, поэтому тесты видят API библиотеки.- Подход сохраняет одинаковую архитектуру тестового target-а независимо от фреймворка.
#include <catch2/catch_test_macros.hpp>
#include "calc.hpp"
TEST_CASE("add works", "[calc]") {
REQUIRE(add(2, 3) == 5);
}
Разбор:
- Заголовок
catch_test_macros.hppоткрывает DSL Catch2 (TEST_CASE,REQUIRE,CHECK). TEST_CASE("add works", "[calc]")задает имя кейса и тег для фильтрации запусков.REQUIREвыполняет строгую проверку: при провале кейс прерывается немедленно.- Выражение внутри
REQUIREнапрямую проверяет бизнес-правило функцииadd.
TEST (GTest) | TEST_CASE (Catch2) | |
|---|---|---|
| Имя набора | первый аргумент TEST(Suite, Name) | теги "[calc]" |
| Жёсткая проверка | ASSERT_EQ | REQUIRE |
| Мягкая проверка | EXPECT_EQ | CHECK |
Запуск только тестов с тегом: catch_tests "[calc]".
Когда что выбирать
| Google Test | Catch2 | |
|---|---|---|
| Экосистема | Google Mock, много примеров в крупных репозиториях | BDD-секции, удобный старт в одном файле |
| Стиль | TEST, EXPECT_* | TEST_CASE, REQUIRE, SECTION |
| Типичное применение | корпоративный C++, Qt-стек | новые библиотеки, учебные репозитории |
Для UI Qt поверх unit-тестов логики — Qt Test (клики, сигналы). Он дополняет тесты чистых функций.
Пример — проверка исключения
TEST(Calc, DivideByZeroThrows) {
EXPECT_THROW(divide(1, 0), std::invalid_argument);
}
Разбор:
EXPECT_THROWпроверяет корректное исключение для негативного сценария.- Первый аргумент — выражение, которое должно бросить исключение.
- Второй аргумент — ожидаемый тип (
std::invalid_argument), то есть часть контракта API. - Такой тест защищает от регрессии, когда разработчик случайно уберет проверку деления на ноль.
EXPECT_THROW(выражение, тип) — тест пройден, если выражение бросило исключение указанного типа. Материал: Обработка исключений в C++.
Советы по организации
- Отдельный
add_executableдля тестов — свойmain, свои зависимости. - Тестируйте публичный API библиотеки, а не
staticдетали в.cpp, если они не часть контракта. - В CI:
cmake --build+ctest -j— одинаково на всех агентах. - Сообщение при падении —
EXPECT_EQ(5, add(2, 3))покажет оба числа; голыйassert— хуже.
После трёх–пяти тестов на calc возьмите задание 3.2 в Практические задания.
Что попробовать
TEST_Fс фикстурой — общая настройка перед каждым тестом.- Три теста — норма, граница, ошибка.
- Связка с CMake:
enable_testing()+gtest_discover_tests.
Ритм написания unit-тестов
На первой неделе с Google Test удобен фиксированный цикл:
- Одна функция под тест.
- "Зелёный" тест на нормальный кейс.
- Граничный случай.
- Кейс с ошибкой / исключением.
ctest --output-on-failure.
Так закрепляется порядок "контракт → реализация", а не правки без обратной связи.
Мини-шаблон набора тестов
TEST(Calc, PositiveNumbers) {
EXPECT_EQ(9, add(4, 5));
}
TEST(Calc, WithZero) {
EXPECT_EQ(4, add(4, 0));
}
TEST(Calc, NegativeNumbers) {
EXPECT_EQ(-3, add(-1, -2));
}
Разбор:
- Первый кейс проверяет "обычный" сценарий с положительными аргументами.
- Второй кейс тестирует границу с нулем и отсутствие побочного влияния второго операнда.
- Третий кейс валидирует корректность для отрицательных чисел.
- Набор покрывает три базовых класса входных данных и служит минимальным regression-pack.
Этот блок уже даёт три важных слоя: обычный сценарий, границу и знак.
Частые ошибки в тестовых проектах
| Проблема | Что сделать |
|---|---|
Тесты не видны в ctest | вызвать enable_testing() и gtest_discover_tests(...) |
| Линкуется приложение, но не тесты | проверить target_link_libraries(calc_tests PRIVATE calc ...) |
| В тестах нет доступа к заголовкам | добавить include-директории через target, а не глобально |
| Тесты "флапают" | убрать зависимость от времени, случайности и внешней сети |
Кейс из практики — тесты "зелёные" и бесполезные
Ситуация: тесты формально проходили, но баг в divide(10, 0) всё равно доехал до релиза.
Что пошло не так: проверяли только "счастливый путь", без границ и ошибок.
Исправление по шаблону "норма + граница + ошибка":
TEST(Calc, DivideNormal) {
EXPECT_EQ(5, divide(10, 2));
}
TEST(Calc, DivideNegative) {
EXPECT_EQ(-5, divide(10, -2));
}
TEST(Calc, DivideByZeroThrows) {
EXPECT_THROW(divide(10, 0), std::invalid_argument);
}
Разбор:
DivideNormalфиксирует стандартный математический сценарий и ожидаемый результат.DivideNegativeпроверяет знак результата при отрицательном делителе.DivideByZeroThrowsзакрепляет обязательное поведение при некорректном входе.- В совокупности это шаблон "норма + граница + ошибка", который снижает риск скрытых дефектов.
Проверка: запуск через ctest --output-on-failure и обязательный прогон в CI.
Результат: тесты становятся контрактом поведения, а не формальностью.