Что такое BDD и почему это не очередная модная фишка?
Вы когда-нибудь писали код, который идеально работает с точки зрения компилятора, но совершенно не делает того, что нужно бизнесу? Знакомо, когда менеджер смотрит на готовую фичу и говорит: «Ну, я думал, будет по-другому». Если да — добро пожаловать в мир BDD (Behaviour-Driven Development) — разработки, управляемой поведением. Это не просто методология, а мост между «хочу, чтобы кнопка была зелёной» и «вот тест, который падает, если кнопка не зелёная».
В отличие от классического TDD (Test-Driven Development), где мы думаем о тестах как о проверке функций, BDD фокусируется на поведении системы с точки зрения пользователя. В контексте C++ это особенно ценно: язык сложный, проекты огромные, и переписывать логику после недопонимания с заказчиком — дорогое удовольствие. BDD на C++ помогает убить двух зайцев: получить документацию, которую поймёт даже проджект-менеджер, и набор автоматических тестов, которые не дадут сломать бизнес-логину.
В этой статье мы разберём, как внедрить behaviour-driven development в реальный C++ проект. Без воды, с кодом и конкретными приёмами.
Основы BDD: язык Gherkin и его роль в C++
Сердце BDD — это сценарии на языке Gherkin. Выглядят они примерно так:
Feature: Перевод средств между счетами
Scenario: Успешный перевод при достаточном балансе
Given у пользователя есть счёт A с балансом 1000 рублей
And у пользователя есть счёт B с балансом 0 рублей
When пользователь переводит 500 рублей со счёта A на счёт B
Then баланс счёта A становится 500 рублей
And баланс счёта B становится 500 рублей
Да, это не C++. Но именно в этом магия: сценарий пишется на человеческом языке, а потом «привязывается» к реальному C++ коду через шаги (steps). Каждая строчка Given, When, Then превращается в функцию, которая дёргает ваши классы и проверяет результаты.
Для C++ существует несколько фреймворков, которые умеют работать с Gherkin. Самые популярные:
- Catch2 с BDD-стилем — лёгкий, встроенный в Catch2, не требует внешних парсеров.
- Cucumber-CPP — полноценная имплементация Cucumber для C++, парсит .feature файлы напрямую.
- Boost.Test — тоже умеет BDD-like синтаксис, но менее гибкий.
Лично я рекомендую начинать с Catch2: он уже есть в 90% C++ проектов, и для BDD не нужно ставить ничего дополнительного. Если проект большой и нужна интеграция с бизнес-аналитиками — берите Cucumber-CPP.
BDD с Catch2: пишем сценарии прямо в коде
Catch2 предоставляет макросы SCENARIO, GIVEN, WHEN, THEN и AND_WHEN/AND_THEN. Это не чистый Gherkin, но стиль максимально приближен. Давайте напишем тест для простого банковского счёта.
Сначала — класс счёта:
// account.h
#pragma once
#include <string>
#include <stdexcept>
class Account {
public:
explicit Account(std::string owner, double initial_balance = 0.0)
: owner_(std::move(owner)), balance_(initial_balance) {}
void deposit(double amount) {
if (amount <= 0) throw std::invalid_argument("Amount must be positive");
balance_ += amount;
}
void withdraw(double amount) {
if (amount > balance_) throw std::runtime_error("Insufficient funds");
if (amount <= 0) throw std::invalid_argument("Amount must be positive");
balance_ -= amount;
}
double balance() const { return balance_; }
const std::string& owner() const { return owner_; }
private:
std::string owner_;
double balance_ = 0.0;
};
А теперь — BDD-тест с использованием Catch2:
// test_account.cpp
#define CATCH_CONFIG_MAIN
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch.hpp>
#include "account.h"
SCENARIO("Перевод средств между счетами", "[banking]") {
GIVEN("У пользователя есть счёт A с балансом 1000 рублей и счёт B с балансом 0 рублей") {
Account accountA("Alice", 1000.0);
Account accountB("Bob", 0.0);
WHEN("Пользователь переводит 500 рублей со счёта A на счёт B") {
accountA.withdraw(500.0);
accountB.deposit(500.0);
THEN("Баланс счёта A становится 500 рублей") {
REQUIRE(accountA.balance() == 500.0);
}
AND_THEN("Баланс счёта B становится 500 рублей") {
REQUIRE(accountB.balance() == 500.0);
}
}
WHEN("Пользователь пытается перевести 1500 рублей (больше, чем есть)") {
THEN("Возникает исключение Insufficient funds") {
REQUIRE_THROWS_AS(accountA.withdraw(1500.0), std::runtime_error);
}
}
}
}
Этот код — классический пример behaviour-driven development на C++. Посмотрите: сценарий читается как спецификация. Вы можете показать этот код менеджеру (если он немного знает английский), и он поймёт, что происходит. При этом тест реально проверяет логику.
Запускается это обычной командой: ./test_account [banking]. Catch2 сам выведет имена сценариев и результаты.
Продвинутый уровень: Cucumber-CPP и Gherkin-файлы
Если вы работаете в команде, где сценарии пишут аналитики или QA, вам нужен Cucumber-CPP. Он парсит внешние .feature файлы и связывает их с C++ кодом через шаги. Рассмотрим пример.
Создадим файл transfer.feature:
Feature: Transfer money
Scenario: Successful transfer
Given account A with balance 1000
And account B with balance 0
When user transfers 500 from A to B
Then account A balance should be 500
And account B balance should be 500
Теперь — C++ код с шагами (используем Cucumber-CPP):
#include <cucumber-cpp/defs.hpp>
#include <string>
#include <map>
#include <cassert>
using namespace cucumber;
std::map<std::string, Account> accounts;
GIVEN("^account (\\w+) with balance (\\d+)$") {
std::string accountName = arg1;
double balance = std::stod(arg2);
accounts[accountName] = Account(accountName, balance);
}
WHEN("^user transfers (\\d+) from (\\w+) to (\\w+)$") {
double amount = std::stod(arg1);
std::string from = arg2;
std::string to = arg3;
accounts[from].withdraw(amount);
accounts[to].