The post has been translated automatically. Original language: Russian
This is the fifth installment in a series about BDD. In the fourth part, "Automation: Tools and CI," we have analyzed the tools. In the sixth, "BDD and the test pyramid: what to check at what level", we will decide exactly where to run such scenarios.
So far, we have analyzed the stages separately. Now let's take one small function and go through it all the way: discussion, scenarios, red test, green test.
Task
Function: booking a meeting room. The rules are educational and conditional.
Step 1. Discovery
The team gathers for a short meeting and draws up a map of examples.
Rule 1: One room cannot be booked at an intersecting time.
• The room is occupied from 10:00 to 11:00, the reservation is from 10:30 to 11:30: refusal.
• The room is occupied from 10:00 to 11:00, the reservation is confirmed from 11:00 to 12:00 (back-to-back is allowed).
Rule 2: one reservation is not longer than two hours.
• Booking from 10:00 to 12:00: confirmed.
• Booking from 10:00 to 12:30: refusal.
Questions:
• How do I cancel my reservation?
• Is it possible to book a time in the past?
• Do I need duplicate bookings?
Pay attention to the result of the discussion: three questions do not prevent you from starting work on the task. The team decided to put them in separate stories and not inflate the current one.
Step 2. Formulation
We write down the examples on Gherkin:
# language: ru
Function: Booking a meeting room
Scenario: Free time can be booked
Given, the meeting room is free
When an employee books it from 10:00 to 11:00
Then the booking is confirmed
Scenario: Overlapping time cannot be booked
Given, the meeting room is busy from 10:00 to 11:00
When an employee books it from 10:30 a.m. to 11:30 a.m.
Then the booking is declined
And the reason for the refusal: time is busy
Scenario: Butt-to-butt booking is allowed
Given, the meeting room is busy from 10:00 to 11:00
When an employee books it from 11:00 to 12:00
Then the booking is confirmed
Scenario: Reservations longer than two hours are not allowed
Given, the meeting room is free
When an employee books it from 10:00 to 12:30
Then the booking is declined
And the reason for the refusal: 2 hours maximumStep 3. Automation: Red test
First, we write the code of the steps (Python, Behave), but the logic itself is not yet there:
from datetime import datetime
from behave import given, when, then
from booking import Room, BookingError
def t(s):
h, m = map(int, s.split(":"))
return datetime(2026, 1, 1, h, m)
@given("meeting room is free")
def step_free(context):
context.room = Room()
@given("the meeting room is busy from {start} to {end}")
def step_busy(context, start, end):
context.room = Room()
context.room.book(t(start), t(end))
@when("the employee books it from {start} to {end}")
def step_book(context, start, end):
try:
context.room.book(t(start), t(end))
context.error = None
except BookingError as e:
context.error = str(e)
@then("booking confirmed")
def step_ok(context):
assert context.error is None
@then("booking declined")
def step_fail(context):
assert context.error is not None
@then("reason for refusal: {reason}")
def step_reason(context, reason):
assert context.error == reasonThe launch gives a red result: there is no booking module yet. This is normal and even useful: we have made sure that the tests really check something.
Step 4. Implementation: Green test
from datetime import timedelta
class BookingError(Exception):
pass
class Room:
def __init__(self):
self.bookings = []
def book(self, start, end):
if end - start > timedelta(hours=2):
raise BookingError("maximum 2 hours")
for s, e in self.bookings:
if start < e and s < end:
raise BookingError("time is busy")
self.bookings.append((start, end))The condition start < e and s < end describes the intersection of the intervals. The end-to-end reservation (11:00 after 11:00) does not fall under it, so the third scenario passes. Launching: all four scenarios are green.
Step 5. What's next
When a new story arrives, such as the cancellation of a reservation, we'll start with a new example, not a code. File.The feature remains in the repository and works as documentation: any team member opens it and sees the booking rules.
What we got
• Three issues were found before the implementation, not after.
• The "butt-to-butt" boundary case is explicitly described, not guessed.
• The behavior is documented in tests that are read without knowledge of Python.
• The code is small because it was written for specific examples.
Result
The entire BDD cycle fit into one small function: discussed, recorded, saw red, made green. In the sixth part, "BDD and the test pyramid: what to check at what level," we'll figure out where to run such scenarios so that they remain fast and stable.
What questions would you add to this map? Which boundary case did we miss?
Это пятая часть серии о BDD. В четвёртой части, «Автоматизация: инструменты и CI», мы разобрали инструменты. В шестой, «BDD и тестовая пирамида: что на каком уровне проверять», решим, где именно запускать такие сценарии.
До сих пор мы разбирали этапы по отдельности. Теперь возьмём одну небольшую функцию и пройдём с ней весь путь: обсуждение, сценарии, красный тест, зелёный тест.
Задача
Функция: бронирование переговорной комнаты. Правила учебные и условные.
Шаг 1. Discovery
Команда собирается на короткую встречу и составляет карту примеров.
Правило 1: одну комнату нельзя забронировать на пересекающееся время.
• Комната занята с 10:00 до 11:00, бронь с 10:30 до 11:30: отказ.
• Комната занята с 10:00 до 11:00, бронь с 11:00 до 12:00: подтверждено (встык допускается).
Правило 2: одна бронь не длиннее двух часов.
• Бронь с 10:00 до 12:00: подтверждено.
• Бронь с 10:00 до 12:30: отказ.
Вопросы:
• Как отменять бронь?
• Можно ли бронировать время в прошлом?
• Нужны ли повторяющиеся брони?
Обратите внимание на результат обсуждения: три вопроса не мешают начать работу над задачей. Команда решила вынести их в отдельные истории и не раздувать текущую.
Шаг 2. Formulation
Примеры записываем на Gherkin:
# language: ru
Функция: Бронирование переговорной
Сценарий: Свободное время можно забронировать
Дано переговорная свободна
Когда сотрудник бронирует её с 10:00 до 11:00
Тогда бронирование подтверждено
Сценарий: Пересекающееся время забронировать нельзя
Дано переговорная занята с 10:00 до 11:00
Когда сотрудник бронирует её с 10:30 до 11:30
Тогда бронирование отклонено
И причина отказа: время занято
Сценарий: Бронь встык допускается
Дано переговорная занята с 10:00 до 11:00
Когда сотрудник бронирует её с 11:00 до 12:00
Тогда бронирование подтверждено
Сценарий: Бронь длиннее двух часов не допускается
Дано переговорная свободна
Когда сотрудник бронирует её с 10:00 до 12:30
Тогда бронирование отклонено
И причина отказа: максимум 2 часаШаг 3. Automation: красный тест
Сначала пишем код шагов (Python, Behave), но самой логики ещё нет:
from datetime import datetime
from behave import given, when, then
from booking import Room, BookingError
def t(s):
h, m = map(int, s.split(":"))
return datetime(2026, 1, 1, h, m)
@given("переговорная свободна")
def step_free(context):
context.room = Room()
@given("переговорная занята с {start} до {end}")
def step_busy(context, start, end):
context.room = Room()
context.room.book(t(start), t(end))
@when("сотрудник бронирует её с {start} до {end}")
def step_book(context, start, end):
try:
context.room.book(t(start), t(end))
context.error = None
except BookingError as e:
context.error = str(e)
@then("бронирование подтверждено")
def step_ok(context):
assert context.error is None
@then("бронирование отклонено")
def step_fail(context):
assert context.error is not None
@then("причина отказа: {reason}")
def step_reason(context, reason):
assert context.error == reasonЗапуск даёт красный результат: модуля booking пока нет. Это нормально и даже полезно: мы убедились, что тесты действительно что-то проверяют.
Шаг 4. Реализация: зелёный тест
from datetime import timedelta
class BookingError(Exception):
pass
class Room:
def __init__(self):
self.bookings = []
def book(self, start, end):
if end - start > timedelta(hours=2):
raise BookingError("максимум 2 часа")
for s, e in self.bookings:
if start < e and s < end:
raise BookingError("время занято")
self.bookings.append((start, end))Условие start < e and s < end описывает пересечение интервалов. Бронь встык (11:00 после 11:00) под него не попадает, поэтому третий сценарий проходит. Запускаем: все четыре сценария зелёные.
Шаг 5. Что дальше
Когда придёт новая история, например отмена брони, мы начнём с нового примера, а не с кода. Файл .feature остаётся в репозитории и работает как документация: любой участник команды открывает его и видит правила бронирования.
Что мы получили
• Три вопроса найдены до реализации, а не после.
• Граничный случай «встык» описан явно, а не угадан.
• Поведение зафиксировано в тестах, которые читаются без знания Python.
• Код небольшой, потому что он писался под конкретные примеры.
Итог
Весь цикл BDD уместился в одну небольшую функцию: обсудили, записали, увидели красный, сделали зелёный. В шестой части, «BDD и тестовая пирамида: что на каком уровне проверять», разберёмся, где запускать такие сценарии, чтобы они оставались быстрыми и стабильными.
Какие вопросы вы бы добавили к этой карте? Какой граничный случай мы пропустили?