Почему BDD на Rust — это не про скучные тесты
Представьте: вы пишете на Rust высоконагруженный сервис. Код безопасный, быстрый, но когда продукт приходит к заказчику, он говорит: «А я думал, кнопка должна работать иначе». Знакомо? Классический разрыв между кодом и ожиданиями. Именно здесь на помощь приходит BDD (Behaviour-Driven Development) — методология, которая переводит бизнес-требования на язык, понятный и менеджеру, и компилятору Rust.
В этой статье мы разберём, как внедрить BDD на Rust на практике. Вы узнаете, как писать сценарии на человеческом языке (Gherkin), автоматизировать их с помощью cucumber и specs, и почему это делает ваш код не только надёжным, но и прозрачным для всей команды. Никакой воды — только живой код и реальные примеры.
Что такое BDD и почему Rust для него идеален
Behaviour-Driven Development — это эволюция TDD. Если TDD фокусируется на «как тестировать код», то BDD отвечает на вопрос «какое поведение мы хотим видеть». Вы пишете сценарии на языке Gherkin (Given-When-Then), которые одновременно являются и тестами, и документацией.
Rust — язык, который обожает строгость и предсказуемость. BDD в Rust даёт:
- Самодокументируемый код — любой разработчик читает сценарий и понимает бизнес-логику.
- Безопасность на уровне типов — компилятор проверяет, что ваши шаги соответствуют структуре.
- Параллельный запуск — тесты BDD в Rust работают быстро благодаря
asyncи многопоточности.
Когда BDD на Rust особенно полезен?
- API-сервисы с чёткими контрактами (REST, GraphQL).
- Системы с бизнес-правилами (финансы, логистика, игры).
- Микросервисы, где важна интеграция между компонентами.
Инструменты для BDD в Rust: Cucumber и Specs
Экосистема Rust предлагает два основных подхода:
1. cucumber — классический BDD-фреймворк
Позволяет писать сценарии на Gherkin (файлы .feature) и связывать их с Rust-кодом. Поддерживает async, кастомные макросы и отчёты.
2. specs — лёгкий и быстрый аналог
Не требует Gherkin-файлов — сценарии пишутся прямо в Rust с помощью макросов. Идеален, если вы не хотите тащить отдельный парсер.
Мы сосредоточимся на cucumber, так как он даёт максимальную гибкость и соответствует стандарту BDD.
Пишем первый BDD-сценарий на Rust
Давайте создадим простой калькулятор скидок. Бизнес-правило: если сумма покупки больше 1000 рублей, скидка 10%. Иначе — 0%.
Шаг 1. Установка зависимостей
Добавьте в Cargo.toml:
[dev-dependencies]
cucumber = "0.20"
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
Шаг 2. Создаём feature-файл
Файл tests/features/discount.feature:
Feature: Расчёт скидки
As a покупатель
I want получить скидку на большую покупку
So that экономить деньги
Scenario: Покупка больше 1000 рублей
Given сумма покупки равна "1500"
When я запрашиваю расчёт скидки
Then размер скидки должен быть "150"
Scenario: Покупка меньше 1000 рублей
Given сумма покупки равна "500"
When я запрашиваю расчёт скидки
Then размер скидки должен быть "0"
Шаг 3. Пишем шаги на Rust
Создайте tests/steps/discount_steps.rs:
use cucumber::{given, then, when, World};
use async_trait::async_trait;
#[derive(Debug, Default)]
struct DiscountWorld {
purchase_amount: f64,
discount: f64,
}
#[async_trait(?Send)]
impl World for DiscountWorld {
// Пустая реализация для простоты
async fn new() -> Self {
Self::default()
}
}
#[given(regex = r"сумма покупки равна \"(\d+)\"")]
async fn set_purchase_amount(world: &mut DiscountWorld, amount: String) {
world.purchase_amount = amount.parse().unwrap();
}
#[when(regex = r"я запрашиваю расчёт скидки")]
async fn calculate_discount(world: &mut DiscountWorld) {
if world.purchase_amount > 1000.0 {
world.discount = world.purchase_amount * 0.1;
} else {
world.discount = 0.0;
}
}
#[then(regex = r"размер скидки должен быть \"(\d+)\"")]
async fn check_discount(world: &mut DiscountWorld, expected: String) {
let expected: f64 = expected.parse().unwrap();
assert_eq!(world.discount, expected);
}
Шаг 4. Запускаем тесты
Создайте tests/main.rs:
use cucumber::Cucumber;
mod steps;
#[tokio::main]
async fn main() {
Cucumber::::new()
.features(&["./tests/features"])
.run()
.await;
}
Запустите: cargo test --test main. Вы увидите зелёные отчёты с прохождением сценариев.
Продвинутые техники BDD на Rust
Параметризация и таблицы
Gherkin поддерживает таблицы данных. Например, для тестирования нескольких сценариев сразу:
Scenario Outline: Проверка скидки для разных сумм
Given сумма покупки равна "<amount>"
When я запрашиваю расчёт скидки
Then размер скидки должен быть "<discount>"
Examples:
| amount | discount |
| 500 | 0 |
| 1500 | 150 |
| 2000 | 200 |
Работа с асинхронностью и состоянием
В реальных проектах часто нужно тестировать базы данных или HTTP-запросы. Cucumber поддерживает async из коробки. Пример с мок-сервером:
#[given("база данных содержит пользователя")]
async fn setup_db(world: &mut AppWorld) {
world.db.insert_user("test_user").await;
}
#[when("я отправляю POST запрос на /login")]
async fn send_login(world: &mut AppWorld) {
world.response = world.client
.post("http://localhost:8080/login")
.json(&serde_json::json!({"user": "test_user"}))
.