Тестирование

Сравнение QTest, Catch2 и GTest для Qt-проектов: что выбрать в 2026

Введение

Хотя QTest — «родной» фреймворк тестирования Qt, многие проекты используют сторонние фреймворки — Catch2 или Google Test (GTest) — даже для тестирования Qt-кода. В статье сравним три варианта по ключевым критериям: интеграция с Qt-специфичными типами, синтаксис, экосистема matchers/assertion, и дадим практические рекомендации по выбору.

Концепция

QTest глубоко интегрирован с Qt: понимает QString, QList, умеет тестировать сигналы через QSignalSpy, поддерживает data-driven тесты через QTest::addColumn/QTest::newRow, и не требует подключения сторонних зависимостей — что особенно удобно в проектах, уже целиком построенных на Qt инфраструктуре сборки.

Catch2 — современный header-only (в более новых версиях — с возможностью предкомпиляции) фреймворк с выразительным синтаксисом BDD-подобных секций (SECTION), мощными matcher’ами и отличной читаемостью вывода при сбоях. Хорошо подходит, когда тестируется логика, не сильно завязанная на специфику Qt, или для смешанных проектов, где Qt — не единственная технология.

GTest — зрелый, широко распространённый в индустрии фреймворк с богатой экосистемой mock-объектов (GMock), хорошей интеграцией с инструментами CI и анализа покрытия, но с более многословным синтаксисом по сравнению с Catch2.

Пример кода

// --- QTest ---
#include <QtTest/QtTest>

class TestMath : public QObject
{
    Q_OBJECT
private slots:
    void testAddition_data()
    {
        QTest::addColumn<int>("a");
        QTest::addColumn<int>("b");
        QTest::addColumn<int>("expected");

        QTest::newRow("positive") << 2 << 3 << 5;
        QTest::newRow("negative") << -1 << -1 << -2;
        QTest::newRow("zero") << 0 << 5 << 5;
    }

    void testAddition()
    {
        QFETCH(int, a);
        QFETCH(int, b);
        QFETCH(int, expected);
        QCOMPARE(a + b, expected);
    }
};
QTEST_APPLESS_MAIN(TestMath)
#include "test_math.moc"
// --- Catch2 ---
#include <catch2/catch_test_macros.hpp>

int add(int a, int b) { return a + b; }

TEST_CASE("Сложение целых чисел", "[math]")
{
    SECTION("положительные числа") {
        REQUIRE(add(2, 3) == 5);
    }
    SECTION("отрицательные числа") {
        REQUIRE(add(-1, -1) == -2);
    }
    SECTION("с нулём") {
        REQUIRE(add(0, 5) == 5);
    }
}
// --- GTest ---
#include <gtest/gtest.h>

int add(int a, int b) { return a + b; }

TEST(MathTest, PositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
}

TEST(MathTest, NegativeNumbers) {
    EXPECT_EQ(add(-1, -1), -2);
}

TEST(MathTest, WithZero) {
    EXPECT_EQ(add(0, 5), 5);
}

Пояснения к коду

QTest-вариант демонстрирует data-driven тестирование через пару методов testAddition_data()/testAddition() — каждая строка, добавленная через QTest::newRow(), превращается в отдельный отчёт о тесте, а QFETCH извлекает соответствующие значения колонок. Catch2-вариант группирует логически связанные проверки через SECTION внутри одного TEST_CASE, что даёт читаемую структуру и независимое выполнение каждой секции. GTest-вариант наиболее многословен, но это и его сила — явная структура TEST(Suite, Name) хорошо ложится на крупные кодовые базы с сотнями тестов, где важна предсказуемая навигация.

Подводные камни

  • Смешивание нескольких тестовых фреймворков в одном проекте без явного разделения усложняет настройку CI и онбординг новых разработчиков — если выбор сделан, стоит придерживаться его консистентно, за исключением обоснованных локальных исключений (например, унаследованный legacy-код на другом фреймворке).
  • Catch2/GTest не понимают Qt-специфичные типы «из коробки». Сравнение QString через REQUIRE/EXPECT_EQ обычно работает благодаря перегрузке operator<< и operator==, но для специфичных Qt-конструкций (сигналы, событийный цикл) придётся писать собственные обёртки или продолжать использовать QSignalSpy поверх стороннего фреймворка — что технически возможно, но усложняет код.
  • Производительность сборки тестов с GTest/Catch2 в большом проекте. Header-only природа некоторых версий Catch2 может заметно увеличивать время компиляции тестовых файлов — в крупных проектах стоит проверить наличие прекомпилированной библиотечной версии или использовать модульность тестов с разделением на несколько исполняемых файлов.
  • Недооценка стоимости миграции существующих QTest-тестов на другой фреймворк. Если проект уже накопил значительное число тестов на QTest, переход на Catch2/GTest «потому что он лучше» редко оправдывает затраты на переписывание — выбор фреймворка для нового проекта и решение о миграции существующего — разные по весу решения.