Введение: почему мы пишем баги, а потом их героически чиним?
Вы когда-нибудь писали код, который работал "вроде бы нормально", а потом, спустя три дня, находили баг, из-за которого всё рушилось? Если да — добро пожаловать в клуб. Если нет — вы либо бог 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
-
<