Юнит-тесты в C++: GoogleTest, Catch2 и doctest
Три популярных фреймворка для юнит-тестов в C++: чем отличаются по скорости сборки, синтаксису и возможностям, что мокать проще и когда какой брать.

Вы завели в проекте пять файлов с тестами, время чистой сборки выросло с 40 секунд до четырёх минут, а половина этого времени уходит на компиляцию самих тестов. Или наоборот: тесты собираются мгновенно, но вам нужны моки, параметризация и красивый вывод при падении, а фреймворк умеет только assert. Выбор тестового фреймворка в C++ — это не вопрос вкуса, а вопрос времени сборки, объёма зависимостей и того, что именно вы собираетесь тестировать. Ниже — сравнение трёх фреймворков, которые реально используют в 2024 году: GoogleTest, Catch2 и doctest.
Разбираем только сам фреймворк тестирования. Сборочная обвязка (CMake, CTest), покрытие (gcov/llvm-cov) и фаззинг — отдельные темы, здесь их не трогаем.
Задача и почему «просто assert» не хватает
Стандартный assert из <cassert> останавливает программу на первом же нарушении и не даёт ни структуры, ни отчёта. На реальном проекте нужно другое: запускать сотни независимых проверок, видеть, какие именно упали и с какими значениями, группировать тесты, гонять один тест на наборе входных данных и подменять зависимости фейками. Всё это и дают фреймворки — но платят за это по-разному.
Три кандидата:
- GoogleTest (googletest, вместе с GoogleMock) — фреймворк от Google, де-факто индустриальный стандарт. Компилируется как библиотека, линкуется к тестовому бинарнику.
- Catch2 — версия 3.x стала compiled-библиотекой (в отличие от header-only v2), с выразительным синтаксисом и секциями.
- doctest — по духу близок к Catch2, но header-only и с упором на минимальное влияние на время компиляции; тесты можно писать прямо рядом с кодом.
Как выглядит один и тот же тест
GoogleTest (v1.14+):
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(MathTest, AddsPositive) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_NE(add(2, 3), 6);
}
Catch2 (v3.x):
#include <catch2/catch_test_macros.hpp>
int add(int a, int b) { return a + b; }
TEST_CASE("add складывает положительные", "[math]") {
REQUIRE(add(2, 3) == 5);
CHECK(add(2, 3) != 6);
}
doctest (v2.4.x):
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
int add(int a, int b) { return a + b; }
TEST_CASE("add складывает положительные") {
CHECK(add(2, 3) == 5);
CHECK(add(2, 3) != 6);
}
Разница в философии видна сразу. GoogleTest требует явно указывать вид сравнения (EXPECT_EQ, EXPECT_LT). Catch2 и doctest используют обычные выражения и умеют разобрать их через шаблонную магию, показывая при падении оба операнда — вы увидите add(2, 3) == 5 с фактическими значениями.
Скорость сборки — главная валюта
В C++ время компиляции тестов бьёт по вам каждый день. Здесь фреймворки расходятся сильнее всего.
GoogleTest — компилируемая библиотека. Заголовки тяжёлые, но собираются один раз; ваши тестовые файлы включают относительно компактные хедеры. GoogleMock добавляет заметный вес за счёт тяжёлых шаблонов.
Catch2 в версии 3 отказался от header-only ради ускорения: раньше единственный огромный заголовок раздувал каждую единицу трансляции. Теперь вы линкуете Catch2::Catch2WithMain, а в файлах подключаете узкие заголовки вроде catch_test_macros.hpp. Это заметно быстрее, чем v2, но всё ещё тяжелее doctest.
doctest построен вокруг одной цели — минимальный оверхед компиляции. Его главный заголовок на порядок легче, а флаг DOCTEST_CONFIG_DISABLE вырезает все тесты из релизной сборки без изменения кода. Это единственный из трёх, который создатели прямо позиционируют для тестов внутри продакшн-файлов.
Если тесты пересобираются на каждой правке и вас это бесит — это не проблема вашего компилятора, а цена header-only фреймворка со сложными шаблонами. doctest выбирают именно за отсутствие этой цены.
Моки: где GoogleTest в одиночку
Здесь простое разделение. GoogleMock (часть репозитория googletest) — полноценная библиотека для создания мок-объектов с матчерами, ожиданиями вызовов и проверкой порядка:
#include <gmock/gmock.h>
class Storage {
public:
virtual ~Storage() = default;
virtual bool save(int id) = 0;
};
class MockStorage : public Storage {
public:
MOCK_METHOD(bool, save, (int id), (override));
};
using ::testing::Return;
TEST(ServiceTest, CallsSaveOnce) {
MockStorage storage;
EXPECT_CALL(storage, save(42)).WillOnce(Return(true));
// ... код, который должен вызвать storage.save(42)
}
У Catch2 и doctest встроенного мокинга нет. Их обычно комбинируют с отдельными библиотеками (например, trompeloeil или FakeIt) либо пишут ручные фейки. Если проект завязан на dependency injection и мокинг — постоянная часть работы, GoogleTest с GoogleMock снимает целый класс решений.
Сравнение
| Критерий | GoogleTest 1.14+ | Catch2 3.x | doctest 2.4.x |
|---|---|---|---|
| Модель поставки | Compiled library | Compiled library | Header-only |
| Влияние на время сборки | Среднее (высокое с gmock) | Среднее | Минимальное |
| Стиль ассертов | EXPECT_EQ / ASSERT_EQ | REQUIRE / CHECK | CHECK / REQUIRE |
| Разбор выражений при падении | Ограниченный | Полный | Полный |
| Встроенные моки | Да (GoogleMock) | Нет | Нет |
| Параметризация | TEST_P / TYPED_TEST | GENERATE, SECTION | SUBCASE, TEST_CASE_TEMPLATE |
| Тесты рядом с кодом | Не рекомендуется | Не рекомендуется | Штатный сценарий |
Параметризация и вложенные сценарии
GoogleTest разделяет параметризацию значениями (TEST_P с INSTANTIATE_TEST_SUITE_P) и типами (TYPED_TEST). Синтаксис многословный, но структура жёсткая и хорошо читается в больших наборах.
Catch2 и doctest идут другим путём — вложенные секции. В Catch2 каждая SECTION внутри TEST_CASE перезапускает тело теста от начала, что удобно для общей настройки с разными ветками:
TEST_CASE("стек ведёт себя корректно", "[stack]") {
std::stack<int> s;
s.push(1);
SECTION("после push размер растёт") {
REQUIRE(s.size() == 1);
}
SECTION("после pop стек пустеет") {
s.pop();
REQUIRE(s.empty());
}
}
У doctest роль секций играет SUBCASE с той же логикой перезапуска. Для перебора значений в doctest есть TEST_CASE_TEMPLATE и генераторы, но параметризация здесь скромнее, чем у GoogleTest.
Когда что выбирать
- Берите GoogleTest, если проект большой, командный и вам регулярно нужны моки. Плюс — огромная экосистема: интеграция с IDE, CI, известные всем идиомы. Минус — вес сборки и многословие. Это выбор по умолчанию для промышленного C++.
- Берите Catch2, если хотите выразительный синтаксис с разбором выражений и секциями, но при этом остаться на компилируемой библиотеке. Хороший баланс между читаемостью и функциональностью, когда моки не критичны.
- Берите doctest, если время компиляции — приоритет, тесты хочется держать рядом с кодом, а проект небольшой или встраиваемый. Он же удобен для библиотек, которые не хотят навязывать тяжёлую тестовую зависимость своим пользователям.
Что ломается на практике
- ODR-нарушения с несколькими main. Если в двух единицах трансляции определить
main(например, забыть, чтоDOCTEST_CONFIG_IMPLEMENT_WITH_MAINуже стоит в другом файле), линкер выдаст multiple definition. Точку входа определяйте ровно один раз. - Смешивание версий Catch2. Код под v2 (единый заголовок
catch.hpp) не соберётся против v3 без правки инклюдов — заголовки разбиты на модули. Мигрируя, проверьте пути включения. - Флаки от общего состояния. Секции Catch2 и doctest перезапускают тело теста, но статические и глобальные переменные между запусками не сбрасываются. Тест, который «иногда падает», часто ловит именно это.
- GoogleMock и NiceMock/StrictMock. По умолчанию неожиданный вызов мока даёт предупреждение, а не падение. Если хотите жёсткую проверку — оборачивайте в
StrictMock, иначе тест пропустит лишние вызовы.
Что делать дальше
Определитесь с одним фреймворком на проект — смешивать два не стоит, это удваивает зависимости и путает CI. Подключите его через ваш пакетный менеджер (vcpkg, Conan) или FetchContent в CMake, а запуск отдайте CTest, чтобы тесты гонялись одной командой. Дальше смотрите на две метрики: время чистой сборки тестового таргета и время прогона. Если сборка растёт быстрее прогона — это сигнал, что фреймворк тяжеловат для вашего темпа правок, и стоит пересмотреть выбор в пользу более лёгкого.
Источники: GoogleTest — официальная документация, doctest — репозиторий на GitHub
Частые вопросы
А если у меня Catch2 версии 2, а не 3?
Тесты не собираются: multiple definition of main. Что делать?
Можно ли использовать GoogleMock без GoogleTest?
Почему тест иногда проходит, а иногда падает?
Что легче всего вставить в существующий CMake-проект?
Какой фреймворк быстрее компилируется?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.