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

Один тест в пяти фреймворках C++: Google Test, Catch2, doctest, Boost.Test, QTest

обновлено 53 просмотров

Введение

Выбрать библиотеку для модульных тестов в C++ непросто: популярных вариантов много, и каждый хвалят по-своему. Лучший способ сравнить их — написать одни и те же тесты на каждом. В этой статье мы протестируем одну простую функцию пятью способами. Фреймворки тестирования C++ в нашем обзоре: Google Test, Catch2, doctest, Boost.Test и QTest. Посмотрим, как выглядит код, как подключить каждую библиотеку в CMake, и разберём, какую из них выбрать для своего проекта.

Тестируемая функция

Возьмём вычисление факториала. Для отрицательных чисел функция возвращает -1 как признак ошибки:

// factorial.h
inline int factorial(int n)
{
    if (n < 0) return -1;   // ошибка
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

В каждом фреймворке проверим положительные значения (0, 5, 10) и обработку отрицательного ввода (-5). Обратите внимание: уже при n = 13 результат не помещается в 32-битный int. Хороший тест должен находить и такие граничные случаи, поэтому в реальном проекте стоит использовать std::uint64_t и проверять переполнение.

Google Test

Самый распространённый фреймворк в индустрии. Богатый набор проверок, параметризованные тесты, встроенная поддержка моков через Google Mock.

// test_gtest.cpp
#include <gtest/gtest.h>
#include "factorial.h"

TEST(FactorialTest, PositiveNumbers) {
    EXPECT_EQ(factorial(0), 1);
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(FactorialTest, NegativeNumber) {
    EXPECT_EQ(factorial(-5), -1);
}
find_package(GTest REQUIRED)
add_executable(test_gtest test_gtest.cpp)
target_link_libraries(test_gtest PRIVATE GTest::gtest_main)

Функция main() берётся из библиотеки gtest_main. EXPECT_* продолжает тест после ошибки, а ASSERT_* сразу его прерывает.

Catch2

Лаконичный синтаксис: проверки пишутся обычными выражениями C++, а при ошибке Catch2 показывает значения обеих частей.

// test_catch2.cpp
#include <catch2/catch_test_macros.hpp>
#include "factorial.h"

TEST_CASE("Factorial calculation") {
    REQUIRE(factorial(0) == 1);
    REQUIRE(factorial(5) == 120);
    REQUIRE(factorial(10) == 3628800);
}

TEST_CASE("Factorial with negative input") {
    REQUIRE(factorial(-5) == -1);
}
find_package(Catch2 3 REQUIRED)
add_executable(test_catch2 test_catch2.cpp)
target_link_libraries(test_catch2 PRIVATE Catch2::Catch2WithMain)

В старых примерах можно встретить #define CATCH_CONFIG_MAIN и #include "catch.hpp". Это синтаксис Catch2 версии 2, где библиотека была одним заголовком. В Catch2 v3 библиотека компилируется отдельно, а main предоставляет цель Catch2WithMain.

doctest

Синтаксис почти как у Catch2, но библиотека намного легче и компилируется быстрее. Это один заголовочный файл, поэтому её удобно просто положить в проект.

// test_doctest.cpp
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "factorial.h"

TEST_CASE("Factorial correctness") {
    CHECK(factorial(0) == 1);
    CHECK(factorial(5) == 120);
    CHECK(factorial(10) == 3628800);
    CHECK(factorial(-5) == -1);
}

Особенность doctest — тесты можно писать прямо рядом с кодом в рабочих исходниках, а в релизной сборке отключать их макросом DOCTEST_CONFIG_DISABLE.

Boost.Test

Часть библиотек Boost. Мощный, но тяжеловесный вариант. Логичен, если проект уже использует Boost.

// test_boost.cpp
#define BOOST_TEST_MODULE FactorialTest
#include <boost/test/included/unit_test.hpp>
#include "factorial.h"

BOOST_AUTO_TEST_CASE(FactorialPositive) {
    BOOST_CHECK_EQUAL(factorial(0), 1);
    BOOST_CHECK_EQUAL(factorial(5), 120);
    BOOST_CHECK_EQUAL(factorial(10), 3628800);
}

BOOST_AUTO_TEST_CASE(FactorialNegative) {
    BOOST_CHECK_EQUAL(factorial(-5), -1);
}

Вариант included/unit_test.hpp подключает всю библиотеку в одну единицу трансляции. Это удобно для старта, но замедляет компиляцию. В больших проектах лучше линковать скомпилированную библиотеку Boost.Test.

QTest

Встроенный фреймворк Qt. Не требует внешних зависимостей и умеет тестировать то, что важно для Qt-приложений: сигналы, события, виджеты, асинхронный код.

// test_qtest.cpp
#include <QtTest>
#include "factorial.h"

class TestFactorial : public QObject
{
    Q_OBJECT
private slots:
    void testPositive() {
        QCOMPARE(factorial(0), 1);
        QCOMPARE(factorial(5), 120);
        QCOMPARE(factorial(10), 3628800);
    }
    void testNegative() {
        QCOMPARE(factorial(-5), -1);
    }
};

QTEST_MAIN(TestFactorial)
#include "test_qtest.moc"
find_package(Qt6 REQUIRED COMPONENTS Test)
set(CMAKE_AUTOMOC ON)
add_executable(test_qtest test_qtest.cpp)
target_link_libraries(test_qtest PRIVATE Qt6::Test)

Строка #include "test_qtest.moc" обязательна, когда класс с Q_OBJECT объявлен прямо в .cpp-файле. Без неё сборка упадёт с ошибками компоновки.

Фреймворки тестирования C++: сравнительная таблица

ФреймворкПодключениеСкорость сборкиСильные стороны
Google TestБиблиотекаСредняяСтандарт индустрии, моки, параметризация
Catch2 v3БиблиотекаСредняяВыразительный синтаксис, BDD-стиль, генераторы
doctestОдин заголовокВысокаяЛёгкость, тесты рядом с кодом
Boost.TestЗаголовки или библиотекаНизкаяГибкость, если Boost уже в проекте
QTestМодуль QtВысокаяСигналы, GUI, события, без внешних зависимостей

Что выбрать

  • Проект на Qt — QTest: он уже есть в комплекте и понимает специфику Qt. Для сложной логики его можно сочетать с Google Test.
  • Новый проект на чистом C++ — Google Test или Catch2.
  • Важна скорость сборки или нужны тесты прямо в исходниках — doctest.
  • Проект уже использует Boost — Boost.Test.

Все пять фреймворков интегрируются с CTest через add_test() и поддерживаются Qt Creator, так что тесты можно запускать прямо из среды.

Заключение

Как видно по примерам, базовые проверки во всех фреймворках тестирования C++ выглядят похоже. Разница проявляется в деталях: способе подключения, скорости сборки, удобстве сообщений об ошибках и дополнительных возможностях. Начните с того фреймворка, который проще встроить в ваш проект, и пишите тесты регулярно — это важнее выбора конкретной библиотеки.