Перейти к основному содержимому

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) задает suite Calc и конкретный сценарий 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_EQREQUIRE
Мягкая проверкаEXPECT_EQCHECK

Запуск только тестов с тегом: catch_tests "[calc]".


Когда что выбирать

Google TestCatch2
Экосистема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 в Практические задания.


Что попробовать

  1. TEST_F с фикстурой — общая настройка перед каждым тестом.
  2. Три теста — норма, граница, ошибка.
  3. Связка с CMake: enable_testing() + gtest_discover_tests.

Ритм написания unit-тестов

На первой неделе с Google Test удобен фиксированный цикл:

  1. Одна функция под тест.
  2. "Зелёный" тест на нормальный кейс.
  3. Граничный случай.
  4. Кейс с ошибкой / исключением.
  5. 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.

Результат: тесты становятся контрактом поведения, а не формальностью.