Тестирование C++ — дисциплина против тихих багов
В языках с богатым runtime ошибку поймает интерпретатор. В C++ ошибка либо упадёт, либо тихо повредит память и проявится через час под нагрузкой. Поэтому тесты в C++ — не «дополнительная вежливость», а первая линия обороны. Здесь собрана терминология и инструменты, которые спрашивают на собеседованиях.
Карта темы
- Unit vs integration vs TDD — что именно проверяет каждый уровень и почему смешивать их вредно.
- Mocks, fakes, stubs — три разных вида тестовых дублей, и почему они не взаимозаменяемы.
- Фреймворки — GoogleTest, Catch2, doctest: что они дают и где различаются.
- Покрытие кода — line/branch/condition coverage, что они не доказывают.
- Тестирование приватных методов — почему это симптом плохого дизайна, и редкие случаи, когда оправдано.
Unit vs integration vs TDD
Unit-тест проверяет одну функцию или класс в изоляции. Зависимости заменяются дублями. Цель — выявить логические ошибки. Должен быть детерминированным, быстрым (миллисекунды) и не трогать диск/сеть/время.
Интеграционный тест проверяет связку модулей с реальными зависимостями: реальная БД, реальный HTTP-клиент, реальная файловая система. Медленнее, нестабильнее, но ловит ошибки контрактов.
TDD (Test-Driven Development) — цикл red → green → refactor: сначала падающий тест, затем минимальная реализация, затем рефакторинг. TDD — про дизайн, а не про покрытие: тесты становятся первым клиентом API и подсвечивают неуклюжие зависимости.
Mocks, fakes, stubs
Три вида тестовых дублей. Путать их — классическая ошибка на собеседованиях.
- Stub — возвращает заранее заданный ответ, не проверяет вызовы. Используется, когда нужен фиксированный ответ зависимости.
- Fake — упрощённая, но рабочая реализация (in-memory БД вместо PostgreSQL). Используется в интеграционных тестах.
- Mock — записывает все вызовы и проверяет, что произошли ожидаемые. Используется для тестирования взаимодействий (Spy-аспект).
В GoogleMock EXPECT_CALL создаёт mock; ручная class FakeStore : public IStore — fake; ON_CALL(...).WillByDefault(Return(42)) — stub. Чрезмерное использование mocks привязывает тесты к структуре кода и ломает их при рефакторинге.
Фреймворки
GoogleTest — де-факто стандарт в индустрии. Богатые матчеры, GMock для дублей, параметризованные тесты, death tests. Минус — заметная инфраструктура, медленная компиляция.
Catch2 — header-only, лаконичный синтаксис на макросах TEST_CASE/SECTION. Хорош для библиотек и небольших проектов. Slower test discovery.
doctest — самый быстрый по компиляции, минималистичный.
Все три интегрируются в CMake через find_package или FetchContent. Не пишите свой собственный фреймворк — он будет хуже всех трёх и без поддержки CI.
Покрытие кода
gcov/llvm-cov собирают статистику исполнения. Различают:
- Line coverage — какие строки выполнились хотя бы раз. Самая слабая метрика.
- Branch coverage — какие ветки
if/switchбыли пройдены. Лучше, ноif (a && b)имеет 4 комбинации, ветка только две. - Condition coverage (MC/DC) — все комбинации условий внутри выражения. Используется в авиационном/автомобильном коде.
⚠️ Высокое покрытие не гарантирует, что код работает — оно гарантирует, что тесты прошли через код. Тест без EXPECT_*/ASSERT_* даёт 100% покрытия и ноль проверок.
Тестирование приватных методов
Прямой совет: не надо. Если приватный метод требует отдельного теста, это сигнал, что он на самом деле — публичная единица поведения, и стоит вынести его в отдельный класс с открытым API.
Если деваться некуда:
friend class FooTestв production-коде — ломает encapsulation, но локально;- наследование с
public-обёрткой только в тесте; private:→protected:+ тестовый наследник — самый чистый вариант.
Не пишите #define private public перед #include — это UB по стандарту и однажды сломается на ровном месте.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Один тест проверяет несколько вещей | При падении непонятно, что именно сломалось |
| Mock везде, где можно вставить fake | Тесты ломаются на каждом рефакторинге |
| Покрытие как KPI | Команда пишет бессмысленные тесты, чтобы поднять цифру |
sleep(1) для синхронизации в тестах | Flaky-тесты, медленный CI |
Тестирование приватных методов через friend | Encapsulation ломается, дизайн не улучшается |
| Использование global state между тестами | Порядок запуска влияет на результат |
| TDD трактуется как «писать тесты после» | Теряется главное — дизайн через тесты |
Значение для собеседований
Тестирование почти всегда — middle/senior часть интервью на C++ позицию в индустрии (бэкенд, embedded, gamedev меньше). Проверяют не «знаешь ли ты GTest», а способность отделить unit от integration, выбрать правильный вид дубля и обосновать, почему «100% покрытие» — это плохая цель.
Что проверяет интервьюер:
- Можешь ли ты объяснить разницу между mock, fake, stub без подглядывания.
- Понимаешь ли, что unit-тест не должен трогать БД/сеть/время.
- Знаешь ли, что покрытие — про прохождение, а не про проверку.
- Понимаешь ли, когда TDD помогает, а когда мешает (например, в research-коде).
Типичный неправильный ответ: «Mock — это когда мы подменяем класс на заглушку.» Это определение stub, не mock. Mock проверяет взаимодействие, stub — нет.