TDD в C++: Разработка через тесты для ленивых гениев

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

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

Начать курс

Введение: почему мы пишем баги, а потом их героически чиним?

Вы когда-нибудь писали код, который работал "вроде бы нормально", а потом, спустя три дня, находили баг, из-за которого всё рушилось? Если да — добро пожаловать в клуб. Если нет — вы либо бог C++, либо просто ещё не запускали продакшн.

Разработка через тестирование (TDD) — это не модная диета для программистов. Это дисциплина, которая переворачивает процесс написания кода с ног на голову. Вместо того чтобы сначала писать логику, а потом судорожно придумывать, как её протестировать, мы делаем наоборот: сначала тест, потом код. Звучит безумно? Возможно. Но именно так рождается код, который не стыдно показать коллегам и который не ломается в пятницу вечером.

В этой статье мы разберём TDD в C++ на реальных примерах: напишем простую библиотеку для работы с числами Фибоначчи, используя цикл Red-Green-Refactor. Вы увидите, как TDD помогает не только избегать багов, но и проектировать интерфейсы.

Что такое TDD и почему C++ без него — как без тормозов?

TDD (Test-Driven Development) — это методология разработки, при которой сначала пишется тест, который заведомо не проходит (Red), затем пишется минимальный код, который заставляет тест пройти (Green), и наконец проводится рефакторинг (Refactor) для улучшения структуры без изменения поведения.

Почему это важно именно для C++? Потому что C++ — язык, где легко выстрелить себе в ногу: неопределённое поведение, утечки памяти, неверные преобразования типов. TDD автоматически создаёт безопасную сетку, которая ловит ошибки на этапе компиляции и выполнения.

Цикл Red-Green-Refactor: как это работает

  • Red: пишем тест, который должен упасть. Это наше "требование" к коду.
  • Green: пишем минимальный код, чтобы тест прошёл. Никакого овер-инжиниринга!
  • Refactor: улучшаем код (убираем дублирование, упрощаем логику) — тесты остаются зелёными.

Звучит просто? Да. Но на практике это требует дисциплины. Давайте попробуем.

Инструменты для TDD в C++: Google Test vs Catch2

Прежде чем писать код, выберем фреймворк для тестирования. Два основных кандидата:

  • Google Test — мощный, популярный, с кучей макросов. Отлично подходит для больших проектов.
  • Catch2 — лёгкий, современный, с удобным синтаксисом. Идеален для учебных примеров и микросервисов.

В этой статье мы будем использовать Catch2 (v3), потому что он минималистичен и не требует лишних настроек. Но если вы предпочитаете Google Test — принципы TDD те же самые.

Практика: пишем TDD для чисел Фибоначчи

Представим, что нам нужно написать функцию fibonacci(int n), которая возвращает n-е число Фибоначчи (0-е = 0, 1-е = 1, 2-е = 1, 3-е = 2 и т.д.).

Шаг 1: Red — пишем тест, который не проходит

Создадим файл test_fib.cpp:

#define CATCH_CONFIG_MAIN
#include <catch2/catch_test_macros.hpp>

// Пока функция не объявлена — это нормально
int fibonacci(int n);

TEST_CASE("Fibonacci numbers", "[fib]") {
    REQUIRE(fibonacci(0) == 0);
    REQUIRE(fibonacci(1) == 1);
    REQUIRE(fibonacci(2) == 1);
    REQUIRE(fibonacci(10) == 55);
}

Пытаемся скомпилировать — получаем ошибку линковки. Тест RED. Отлично!

Шаг 2: Green — пишем минимальный код, чтобы тест прошёл

Создадим файл fib.cpp:

int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

Компилируем и запускаем тесты. Они проходят? Да, но это неэффективно. Однако наша цель — Green, а не совершенство. Пока оставляем как есть.

Шаг 3: Refactor — улучшаем код, не ломая тесты

Рекурсивное решение — O(2^n). Давайте заменим на итеративное:

int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; ++i) {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}

Запускаем тесты — они всё ещё зелёные. Рефакторинг удался. При этом мы не поломали ни одного требования.

Добавляем новые требования: обработка ошибок

Теперь представим, что нам нужно обрабатывать отрицательные числа. В TDD это просто — пишем новый тест, который упадёт.

Шаг 4: Red — тест на отрицательный аргумент

TEST_CASE("Fibonacci with negative input", "[fib][error]") {
    REQUIRE_THROWS_AS(fibonacci(-1), std::invalid_argument);
}

Тест не компилируется — у нас нет исключения. RED.

Шаг 5: Green — добавляем проверку

#include <stdexcept>

int fibonacci(int n) {
    if (n < 0) throw std::invalid_argument("n must be non-negative");
    if (n <= 1) return n;
    // ... остальной код
}

Тесты проходят. Green.

Продвинутый TDD: тестируем классы и состояния

TDD отлично работает не только с функциями, но и с классами. Допустим, мы пишем простой калькулятор:

class Calculator {
public:
    int add(int a, int b);
    int multiply(int a, int b);
};

Пишем тест до реализации:

TEST_CASE("Calculator operations", "[calc]") {
    Calculator calc;
    REQUIRE(calc.add(2, 3) == 5);
    REQUIRE(calc.multiply(2, 3) == 6);
}

Затем реализуем минимально:

int Calculator::add(int a, int b) { return a + b; }
int Calculator::multiply(int a, int b) { return a * b; }

И только потом, если нужно, рефакторим (например, добавляем защиту от переполнения).

Частые ошибки новичков в TDD

    <

Блоги

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