GoogleTest на практике: рабочие приёмы и типичные ошибки
Как писать тесты на GoogleTest так, чтобы падение показывало причину, а не факт. Параметризация, фикстуры, моки и подводные камни ASSERT против EXPECT.

У вас есть тест TEST(MathTest, DivideWorks), он падает, и в логе одна строка: expected: true, actual: false. Что именно сравнивалось, с какими числами — неизвестно, придётся лезть в код и добавлять std::cout. Это не проблема GoogleTest, это проблема того, как тест написан. Ниже — приёмы, которые превращают падение теста в готовый диагноз, и ошибки, из-за которых зелёные тесты пропускают реальные баги.
Статья про GoogleTest 1.14+ (актуальная ветка на момент написания, собирается через CMake и FetchContent). Про интеграцию с CMake здесь — минимум, только то, что нужно для запуска примеров; сборка тестового таргета — отдельная тема.
ASSERT или EXPECT: разница не косметическая
Два семейства макросов путают чаще всего. Разница в том, что происходит после провала проверки:
EXPECT_*— фиксирует провал и продолжает выполнять тело теста дальше;ASSERT_*— фиксирует провал и немедленно выходит из текущей функции черезreturn.
Отсюда правило: ASSERT ставят там, где дальнейшие проверки не имеют смысла и приведут к падению всего процесса. Классика — разыменование указателя:
// Если объект не создался, продолжать нельзя: дальше UB
auto* parser = factory.create("json");
ASSERT_NE(parser, nullptr);
EXPECT_EQ(parser->format(), "json");
EXPECT_TRUE(parser->isReady());Если тут поставить EXPECT_NE, то при nullptr следующая строка разыменует его и уронит весь тестовый бинарник с segfault — вы потеряете не один тест, а все, что должны были выполниться после.
Правило простое: EXPECT для независимых проверок, ASSERT — только когда следующая строка física не может выполниться без успеха предыдущей. Не заменяйте все EXPECT на ASSERT «на всякий случай»: так вы теряете остальные диагнозы при первом же провале.
Специализированные матчеры вместо голого EXPECT_TRUE
Первая же ошибка новичка — писать EXPECT_TRUE(a == b). При провале вы увидите только false. Используйте EXPECT_EQ(a, b) — в логе появятся оба значения:
EXPECT_EQ(sum, 42); // при провале: which is 40 vs 42
EXPECT_TRUE(sum == 42); // при провале: только Value of: sum == 42, falseДля строк EXPECT_EQ работает с std::string и печатает обе, а вот для const char* нужен EXPECT_STREQ — иначе сравните указатели, а не содержимое. Для чисел с плавающей точкой — EXPECT_DOUBLE_EQ или EXPECT_NEAR(value, expected, tolerance), потому что EXPECT_EQ(0.1 + 0.2, 0.3) провалится из-за представления double.
Фикстуры: убрать копипасту подготовки
Когда в десяти тестах одинаковая настройка объекта, выносите её в фикстуру. Класс наследует ::testing::Test, метод SetUp() вызывается перед каждым тестом, TearDown() — после:
#include <gtest/gtest.h>
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
queue.push(10);
queue.push(20);
}
// TearDown можно не писать, если чистить нечего:
// деструкторы полей отработают сами
IntQueue queue;
};
TEST_F(QueueTest, PopReturnsFirstPushed) {
EXPECT_EQ(queue.pop(), 10);
}
TEST_F(QueueTest, SizeAfterTwoPushes) {
EXPECT_EQ(queue.size(), 2);
}Обратите внимание: макрос — TEST_F (от fixture), а первый аргумент должен точно совпадать с именем класса. Каждый тест получает свежий экземпляр фикстуры — состояние не течёт между тестами. Это осознанное дизайн-решение GoogleTest: тесты не должны зависеть от порядка выполнения.
Где фикстура ломается
Соблазн — сложить в фикстуру дорогой ресурс (соединение с БД, загрузку большого файла) и переиспользовать между тестами. Так делать нельзя через обычный SetUp: он вызывается на каждый тест. Для ресурса «один на весь набор» есть статические SetUpTestSuite() и TearDownTestSuite() — но тогда тесты начинают делить состояние, и вы возвращаете себе зависимость от порядка. Взвешивайте: чаще дешевле поднять ресурс заново, чем ловить плавающие падения.
Параметризация: один тест на десяток входов
Копировать тест ради разных входных данных — путь к тому, что при изменении логики придётся править десять мест. GoogleTest даёт TEST_P с генератором значений:
struct DivCase {
int dividend;
int divisor;
int expected;
};
class DivideTest : public ::testing::TestWithParam<DivCase> {};
TEST_P(DivideTest, Divides) {
const auto& c = GetParam();
EXPECT_EQ(c.dividend / c.divisor, c.expected);
}
INSTANTIATE_TEST_SUITE_P(
Integers, DivideTest,
::testing::Values(
DivCase{10, 2, 5},
DivCase{9, 3, 3},
DivCase{7, 1, 7}
));Один провалившийся случай не гасит остальные — каждый набор параметров превращается в отдельный тест с собственным именем в отчёте. Это заметно лучше цикла внутри одного TEST, где первый же ASSERT оборвёт проверку остальных значений.
Сравнение подходов: как оформлять повторяющиеся тесты
| Подход | Когда брать | Цена |
|---|---|---|
| Отдельные TEST | Проверки логически разные | Копипаста при общей подготовке |
| Фикстура TEST_F | Общая настройка, разные проверки | Свежий объект на каждый тест |
| Параметризация TEST_P | Одна логика, много входов | Больше кода на объявление, сложнее читать |
| Цикл внутри TEST | Почти никогда | Первый провал скрывает остальные |
Моки через GoogleMock
Когда тестируемый класс зависит от интерфейса, который в тесте поднимать дорого или невозможно (сеть, файловая система, время), подставляют мок. GoogleMock идёт в комплекте с GoogleTest:
#include <gmock/gmock.h>
class IStorage {
public:
virtual ~IStorage() = default;
virtual bool save(const std::string& key, int value) = 0;
virtual int load(const std::string& key) = 0;
};
class MockStorage : public IStorage {
public:
MOCK_METHOD(bool, save, (const std::string& key, int value), (override));
MOCK_METHOD(int, load, (const std::string& key), (override));
};
using ::testing::Return;
using ::testing::_;
TEST(CacheTest, LoadsFromStorageOnMiss) {
MockStorage storage;
EXPECT_CALL(storage, load("x")).WillOnce(Return(99));
Cache cache(&storage);
EXPECT_EQ(cache.get("x"), 99);
}Ключевая деталь: EXPECT_CALL — это не просто заглушка, а проверка ожидания. Если load("x") не будет вызван ровно один раз, тест провалится при разрушении мока с сообщением о неудовлетворённом ожидании. Метод, у которого нет EXPECT_CALL, но который вызывается, даст предупреждение uninteresting call — не ошибку, но сигнал, что вы что-то не описали.
Если тест ведёт себя не так
- Тест не запускается, «0 tests ran». Имя фикстуры в
TEST_Fне совпало с классом, либо забыли слинковатьgtest_mainи нетmain(). Проверьте, что таргет линкуется сGTest::gtest_main, а не только сGTest::gtest. - «This mock object should be deleted» или утечка мока. Мок разрушается позже, чем проверяются ожидания, либо владение указателем ушло не туда. Держите мок как локальную переменную теста, а тестируемому объекту отдавайте сырой указатель или reference.
- EXPECT_EQ на float всегда падает. Замените на
EXPECT_NEARс явным допуском илиEXPECT_FLOAT_EQ. - Тесты падают через раз. Скорее всего, состояние течёт между тестами через статику или синглтон. Свежая фикстура на каждый тест эту проблему не решает, если объект хранит состояние в статических полях.
- Segfault вместо понятного отчёта. Где-то
EXPECT_*вместоASSERT_*перед разыменованием указателя или доступом к пустому контейнеру.
Что настроить дальше
Когда тесты пишутся и запускаются, следующий шаг — включить их в CI и смотреть на два показателя. Первый — покрытие через gcov/lcov (для GCC/Clang), чтобы видеть непроверенные ветки, но не гнаться за 100%: покрытая строка не значит проверенное поведение. Второй — время прогона: если набор растёт до минут, разберитесь, какие тесты фактически интеграционные, и вынесите их в отдельный таргет с фильтром --gtest_filter. Флаг --gtest_repeat вместе с --gtest_shuffle помогает вылавливать тесты, зависящие от порядка. Точный синтаксис фильтров и генераторов параметров сверяйте с официальной документацией GoogleTest — API между минорными версиями стабилен, но матчеры GoogleMock иногда пополняются.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: GoogleTest User's Guide (официальная документация), GoogleTest на GitHub
Частые вопросы
У меня старая версия GoogleTest, макрос MOCK_METHOD не компилируется
Тесты собираются, но при запуске пишет '0 tests from 0 test suites'
Обязательно ли писать TearDown, если использую фикстуру
Чем EXPECT_CALL отличается от ON_CALL в GoogleMock
Как запустить только один тест из большого набора
Почему тесты падают по-разному при каждом запуске
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.