doctest для C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

doctest для C++: полное руководство с примерами

1. Что это и зачем нужна?

doctest — это легковесный, самодокументируемый фреймворк для модульного тестирования на языке C++. Его главная особенность — минимальное время компиляции и простота использования. В отличие от тяжеловесных фреймворков, таких как Google Test или Boost.Test, doctest представляет собой заголовочную библиотеку (header-only), которую можно подключить одной строкой #include <doctest/doctest.h>. Это делает её идеальным выбором для проектов, где важна скорость сборки и отсутствие внешних зависимостей.

Зачем нужен doctest? В современной разработке на C++ тестирование кода — это не роскошь, а необходимость. doctest позволяет писать тесты прямо рядом с реализацией функций, что улучшает читаемость кода и облегчает его поддержку. Вы можете встраивать тесты в заголовочные файлы, и они будут автоматически исключены из финальной сборки при компиляции без флага -DDOCTEST_CONFIG_DISABLE. Это даёт уникальную возможность: тесты живут вместе с кодом, не влияя на производительность релизной версии.

doctest поддерживает все современные стандарты C++ (C++11 и выше), работает на всех основных платформах (Windows, Linux, macOS) и легко интегрируется с системами непрерывной интеграции (CI). Он предоставляет богатый набор макросов для проверки утверждений, группировки тестов, работы с исключениями и генерации отчётов. Благодаря своей лёгкости, doctest часто используется в embedded-разработке, игровых движках и других проектах, где каждый килобайт и миллисекунда компиляции на счету.

2. Установка

doctest — это header-only библиотека, поэтому установка сводится к копированию заголовочного файла в ваш проект. Самый простой способ — использовать менеджер пакетов vcpkg или Conan, либо скачать файл вручную.

Вариант 1: Установка через vcpkg (рекомендуется)

# Установите vcpkg, если ещё не установили:
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh  # Linux/macOS
# или .\bootstrap-vcpkg.bat (Windows)

# Установите doctest:
./vcpkg install doctest

# Подключите к проекту через CMake:
# find_package(doctest CONFIG REQUIRED)
# target_link_libraries(your_target PRIVATE doctest::doctest)

Вариант 2: Установка через Conan

# Добавьте в conanfile.txt:
[requires]
doctest/2.4.11

[generators]
cmake

# Установите:
conan install . --build=missing

Вариант 3: Ручная установка (просто скопировать файл)

# Скачайте последнюю версию с GitHub:
wget https://raw.githubusercontent.com/doctest/doctest/master/doctest/doctest.h

# Поместите файл в папку include вашего проекта
# Затем в коде просто используйте:
# #include "doctest.h"

После установки убедитесь, что компилятор может найти заголовочный файл. Для компиляции тестов достаточно любой современной версии GCC, Clang или MSVC.

3. Быстрый старт — минимальный рабочий пример

Создайте файл test_example.cpp со следующим содержимым:

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

TEST_CASE("testing the factorial function") {
    CHECK(factorial(0) == 1);
    CHECK(factorial(1) == 1);
    CHECK(factorial(5) == 120);
    CHECK(factorial(10) == 3628800);
}

Скомпилируйте и запустите:

g++ -std=c++11 test_example.cpp -o test_example
./test_example

Вы увидите вывод:

===========================================================================
[doctest] test cases:    1 |    1 passed |    0 failed |    0 skipped
[doctest] assertions:    4 |    4 passed |    0 failed |
[doctest] Status: SUCCESS!

Поздравляю! Вы написали и запустили свой первый тест с doctest. Обратите внимание на макрос DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN — он создаёт точку входа main() для тестов. Если вы хотите написать свой main(), используйте DOCTEST_CONFIG_IMPLEMENT и вызовите doctest::Context вручную.

4. Основные методы/функции/классы

doctest предоставляет мощный набор макросов и классов. Ниже приведены 8 ключевых элементов с сигнатурами, описанием и примерами.

4.1. TEST_CASE — определение тестового случая

Сигнатура: TEST_CASE(const char* name)

Описание: Макрос для определения тестового случая. Все проверки внутри него выполняются как единый тест. Имя теста должно быть уникальным в пределах файла.

TEST_CASE("vector addition works correctly") {
    std::vector<int> a = {1, 2, 3};
    std::vector<int> b = {4, 5, 6};
    std::vector<int> result;
    for (size_t i = 0; i < a.size(); ++i) {
        result.push_back(a[i] + b[i]);
    }
    CHECK(result[0] == 5);
    CHECK(result[1] == 7);
    CHECK(result[2] == 9);
}

4.2. CHECK — проверка утверждения (без остановки)

Сигнатура: CHECK(expression)

Описание: Проверяет, что выражение истинно. Если проверка не проходит, тест продолжает выполняться (в отличие от REQUIRE). Выводит подробную информацию об ошибке.

TEST_CASE("CHECK does not stop on failure") {
    CHECK(1 == 2);  // Эта проверка не пройдёт
    CHECK(2 == 2);  // Но эта всё равно выполнится
    // В выводе будет два assertion: один failed, один passed
}

4.3. REQUIRE — критическая проверка (с остановкой)

Сигнатура: REQUIRE(expression)

Описание: Проверяет, что выражение истинно. Если проверка не проходит, выполнение текущего тестового случая немедленно прекращается. Используется для критических предусловий.

TEST_CASE("REQUIRE stops on failure") {
    int* ptr = nullptr;
    REQUIRE(ptr != nullptr);  // Тест остановится здесь
    *ptr = 42;  // Этот код не выполнится
}

4.4. CHECK_FALSE — проверка ложности

Сигнатура: CHECK_FALSE(expression)

Описание: Проверяет, что выражение ложно. Аналог CHECK(!expression), но с более понятным сообщением об ошибке.

TEST_CASE("CHECK_FALSE example") {
    bool is_empty = true;
    CHECK_FALSE(is_empty);  // Не пройдёт, т.к. is_empty == true
}

4.5. CHECK_THROWS — проверка исключения

Сигнатура: CHECK_THROWS(expression)

Описание: Проверяет, что выражение выбрасывает любое исключение. Если исключение не выброшено, тест считается проваленным.

void might_throw(bool should_throw) {
    if (should_throw) {
        throw std::runtime_error("error");
    }
}

TEST_CASE("CHECK_THROWS example") {
    CHECK_THROWS(might_throw(true));   // Пройдёт
    CHECK_THROWS(might_throw(false));  // Не пройдёт
}

4.6. CHECK_THROWS_AS — проверка типа исключения

Сигнатура: CHECK_THROWS_AS(expression, exception_type)

Описание: Проверяет, что выражение выбрасывает исключение конкретного типа. Позволяет убедиться, что выбрасывается именно ожидаемое исключение, а не любое.

void divide_by_zero() {
    throw std::invalid_argument("division by zero");
}

TEST_CASE("CHECK_THROWS_AS example") {
    CHECK_THROWS_AS(divide_by_zero(), std::invalid_argument);  // Пройдёт
    CHECK_THROWS_AS(divide_by_zero(), std::runtime_error);     // Не пройдёт
}

4.7. SUBCASE — вложенные тестовые сценарии

Сигнатура: SUBCASE(const char* name)

Описание: Позволяет создавать вложенные сценарии внутри одного TEST_CASE. Каждый SUBCASE выполняется независимо, начиная с начала TEST_CASE. Идеально для тестирования с разными входными данными без дублирования кода.

TEST_CASE("string operations") {
    std::string s = "hello";
    
    SUBCASE("length") {
        CHECK(s.length() == 5);
    }
    
    SUBCASE("uppercase") {
        std::transform(s.begin(), s.end(), s.begin(), ::toupper);
        CHECK(s == "HELLO");
    }
    
    SUBCASE("substring") {
        CHECK(s.substr(0, 2) == "he");
    }
}

4.8. INFO — добавление контекстной информации

Сигнатура: INFO(expression)

Описание: Добавляет дополнительную информацию к сообщению об ошибке. Полезно для отладки, когда нужно понять, при каких данных произошёл сбой. Информация выводится только при провале проверки.

TEST_CASE("INFO example") {
    std::vector<int> data = {1, 2, 3, 4, 5};
    for (size_t i = 0; i < data.size(); ++i) {
        INFO("Index: ", i, ", Value: ", data[i]);
        CHECK(data[i] > 0);  // Если проверка не пройдёт, увидим индекс и значение
    }
}

5. Реальный практический пример

Рассмотрим реальный сценарий: разработка класса BankAccount для банковской системы. Мы напишем тесты, покрывающие основные операции: создание счёта, пополнение, снятие, перевод и обработку ошибок.

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include <string>
#include <stdexcept>
#include <vector>

// Класс банковского счёта
class BankAccount {
private:
    std::string owner_name;
    double balance;
    std::string account_number;
    static int next_account_number;

public:
    BankAccount(const std::string& name, double initial_balance = 0.0)
        : owner_name(name), balance(initial_balance) {
        if (initial_balance < 0) {
            throw std::invalid_argument("Initial balance cannot be negative");
        }
        if (name.empty()) {
            throw std::invalid_argument("Owner name cannot be empty");
        }
        account_number = "ACC" + std::to_string(next_account_number++);
    }

    void deposit(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Deposit amount must be positive");
        }
        balance += amount;
    }

    void withdraw(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Withdrawal amount must be positive");
        }
        if (amount > balance) {
            throw std::runtime_error("Insufficient funds");
        }
        balance -= amount;
    }

    double get_balance() const { return balance; }
    std::string get_owner() const { return owner_name; }
    std::string get_account_number() const { return account_number; }

    static bool transfer(BankAccount& from, BankAccount& to, double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Transfer amount must be positive");
        }
        if (from.balance < amount) {
            return false;  // Недостаточно средств
        }
        from.withdraw(amount);
        to.deposit(amount);
        return true;
    }
};

int BankAccount::next_account_number = 1000;

// ===================== ТЕСТЫ =====================

TEST_CASE("BankAccount creation") {
    SUBCASE("Create account with valid data") {
        BankAccount acc("Alice", 1000.0);
        CHECK(acc.get_owner() == "Alice");
        CHECK(acc.get_balance() == 1000.0);
        CHECK(!acc.get_account_number().empty());
    }

    SUBCASE("Create account with zero balance") {
        BankAccount acc("Bob");
        CHECK(acc.get_balance() == 0.0);
    }

    SUBCASE("Create account with negative balance throws") {
        CHECK_THROWS_AS(BankAccount("Charlie", -100.0), std::invalid_argument);
    }

    SUBCASE("Create account with empty name throws") {
        CHECK_THROWS_AS(BankAccount("", 500.0), std::invalid_argument);
    }
}

TEST_CASE("BankAccount deposit") {
    BankAccount acc("Dave", 500.0);

    SUBCASE("Deposit positive amount") {
        acc.deposit(200.0);
        CHECK(acc.get_balance() == 700.0);
    }

    SUBCASE("Deposit zero amount throws") {
        CHECK_THROWS_AS(acc.deposit(0.0), std::invalid_argument);
    }

    SUBCASE("Deposit negative amount throws") {
        CHECK_THROWS_AS(acc.deposit(-50.0), std::invalid_argument);
    }

    SUBCASE("Multiple deposits") {
        acc.d                    

Рекомендаций