
На протяжении истории люди придумывали различные подходы и приёмы, как разрабатывать более качественные и поддерживаемые приложения. В этой статье я бы хотел рассказать о такой методологии разработки, как BDD (Behaviour Driven Development). Но прежде чем перейти непосредственно к гвоздю программы — небольшое вступление.
Думаю, бол��шинство разработчиков согласятся с мыслью о том, что покрытый юнит-тестами код лучше, чем непокрытый. Действительно, тесты позволяют эффективно следить за работоспособностью кода, вовремя отлавливать нерабочие изменения. А ещё из наличия юнитов обычно следует то, что код разбит на логические модули и каждый класс/функция имеет одну зону ответственности (привет SOLID). Тот, кому доводилось писать тест на большую функцию с несколькими зонами ответственности знает, что тесты на такую функцию обречены быть хрупкими и падать при малейшем изменении. Это заставляет задуматься о том, чтобы не писать всё "в одной портянке", а писать гибкий код, поделённый на модули. С таким кодом, как правило, приятнее работать, т.к. приходится держать в уме меньше информации.
В какой-то момент люди сделали вывод, что раз код хороший если он тестируемый, тогда давайте мы сначала напишем тесты на этот код, а уже потом сам код. И так придумали методологию...
Test Driven Development (TDD)
Test Driven Development (TDD) — это подход к разработке программного обеспечения, который ставит тестирование на первое место. В TDD разработчики сначала пишут тесты для новой функциональности, а затем пишут код, который позволит этим тестам пройти. Это отличается от традиционного подхода, когда тесты пишутся после кода, однако привносит свои преимущества:
Повышение качества кода: Тесты обеспечивают быструю обратную связь о том, работает ли код так, как предполагалось.
Упрощение рефакторинга: Тесты служат "сетью безопасности", позволяя разработчикам вносить изменения в код без страха сломать что-то.
Документация: Тесты могут служить формой документации, показывая, как предполагается использование кода.
Программист при написании тестов по сути занимается проектированием и составлением требований к модулю, заранее продумывая что это будет за модуль и за что он будет отвечать.

Конечно, за всё приходится платить, и TDD не является исключением:
Замедление процесса разработки: Написание тестов занимает время, которое могло бы быть потрачено на написание нового кода.
Трудность написания хороших тестов: Написание эффективных тестов — это навык, который требует практики и опыта.
Риск переосмысления: Существует риск, что разработчики могут проводить слишком много времени, пытаясь заставить тесты пройти, вместо того чтобы думать о лучшем дизайне системы.
Behavior Driven Development как развитие TDD
Людям хотелось использовать TDD подход не только в вопросах реализации того или иного функционала, но и для более широких вещей. Хотелось иметь достаточно ясные тесты, взглянув на которые у человека не составило бы труда понять, что умеет делать определённая часть приложения. Проблема в том, что классические тесты не всегда могут быть сразу понятными для человека, увидевшего ваше приложение в первый раз. Здесь и появляется BDD.
Behavior Driven Development (BDD) — это подход к разработке программного обеспечения, который вырос из TDD. В то время как TDD сосредоточен на тестировании отдельных модулей кода, BDD расширяет этот подход, сосредоточиваясь на поведении системы в целом.
При написании BDD тестов реализуются степы, или шаги — небольшие функции, которые выполняют одно определённое действие юзера. BDD использует естественный язык и конкретные примеры для описания ожидаемого поведения системы. Это помогает улучшить коммуникацию между разработчиками, тестировщиками и непрограммистами, такими как менеджеры проектов или стейкхолдеры.
Принципы BDD
BDD основывается на нескольких ключевых принципах:
Описание поведения: Вместо того чтобы сосредоточиваться на технических деталях реализации, BDD фокусируется на описании ожидаемого поведения системы с точки зрения пользователя или стейкхолдера. Это помогает убедиться, что разрабатываемые функции действительно соответствуют потребностям пользователей.
Использование естественного языка: BDD использует естественный язык для описания сценариев тестирования, что делает их понятными не только для разработчиков и тестировщиков, но и для менеджеров проектов, бизнес-аналитиков и других участников команды.
Сотрудничество и коммуникация: BDD подчеркивает важность сотрудничества и общения между всеми участниками команды. Это помогает обеспечить общее понимание целей и требований проекта.
Примеры: BDD использует конкретные примеры для описания ожидаемого поведения. Это помогает уточнить требования и обеспечивает ясность в отношении того, что должна делать система.

Как и любой подход, BDD имеет свои преимущества и недостатки.
Плюсы:
Повышение качества коммуникации: BDD использует естественный язык для описания ожидаемого поведения, что делает его понятным для всех участников команды, включая непрограммистов.
Сосредоточение на бизнес-целях: BDD помогает команде сосредоточиться на достижении конкретных бизнес-целей, а не просто на написании кода.
Поддержка автоматизации тестирования: BDD поддерживает автоматизацию тестирования, что позволяет быстро и эффективно проверять поведение системы.
Минусы:
Сложность внедрения: Внедрение BDD может потребовать значительных изменений в процессах разработки и тестирования, что может быть сложно для некоторых команд.
Требуется обучение: Для эффективного использования BDD команде может потребоваться обучение, особенно для понимания и написания сценариев на естественном языке.
Риск неправильного понимания: Если сценарии BDD написаны неправильно или нечетко, это может привести к неправильному пониманию требований или ожидаемого поведения системы.
При разработке BDD тестов можно использовать два подхода:
Тест-кейсы и реализация степов на классическом ЯП.
Сами тесты должны выглядеть максимально понятно и не содержать сложной логики. Условный BDD-тест на Python может выглядеть так:class FeatureCalculator: """ As a user I want to perform some math ops """ def test_one_plus_one(self): """ I can successfully add one to one """ # Given self.working_calculator() # When self.i_add(1, 1) # Then self.result_is(2) def test_one_plus_one_broken_calculator(self): """ I try to add one to one using broken calculator """ # Given self.broken_calculator() # When self.i_add(1, 1) # Then self.result_is(None) def working_calculator(self): ... def broken_calculator(self): ... def i_add(self, x, y): ... def result_is(self, x): ...Тест-кейсы на Gherkin, реализация степов на классическом ЯП.
Gherkin — это язык, созданный специально для описания поведения систем. Содержит небольшое количество ключевых слов, которое тем не менее является достаточным для составления тестовых сценариев. Тот же самый тест, но на Gherkin:Feature: Calculator As a user I want to perform some math ops Scenario: I can successfully add one to one Given working calculator When I add 1 to 1 Then result is 2 Scenario: I try to add one to one using broken calculator Given broken calculator When I add 1 to 1 Then result is None
Практическая часть: Примеры BDD тестов с использованием библиотеки python-behave
Для практики давайте покроем BDD тестами некоторое приложение. На SwaggerHub выложен в открытом доступе Swagger Petstore — REST API для управления неким "магазином домашних питомцев". Попробуем покрыть тестами endpoint POST /pet, отвечающий за добавление нового питомца в магазин.
Писать тесты я буду с использованием Gherkin. Для реализации таких тестов я выбрал Python 3.10 + фреймворк python-behave. Установить его можно через pip:
pip install behave
Впрочем, BDD фреймворков существует много для таких языков как Java, C#, Go, JavaScript и другие.
Запуск BDD тестов происходит через консоль командой behave, либо через плагины для вашего любимого инструмента разработки (например в PyCharm Professional поддержка BDD есть из коробки).
Примеры BDD тестов
Создадим новый feature-файл:
features/pet.feature
Feature: Everything about your Pets As user I want to add, update, and delete pets in the pet store # здесь будут располагаться тесты # Scenario: ...
Для первого запуска напишем простой тест, который будет посылать POST запрос на API и ожидать, что получит в ответ 200.
features/pet.feature
Scenario: I successfully create new empty pet When I make POST request to "/pet" with body Then Response is 200
Но этот тест не запустится прямо сейчас, потому что мы не реализовали поведение для шагов I make POST request to "/pet" with body и Response is 200:
Feature: Everything about your Pets # features/pet.feature:1 As user I want to add, update, and delete pets in the pet store Scenario: I successfully create new empty pet # features/pet.feature:4 When I make POST request to "/pet" with body # None Then Response is 200 # None Failing scenarios: features/pet.feature:4 I successfully create new empty pet 0 features passed, 1 failed, 0 skipped 0 scenarios passed, 1 failed, 0 skipped 0 steps passed, 0 failed, 0 skipped, 2 undefined Took 0m0.000s You can implement step definitions for undefined steps with these snippets: @when(u'I make POST request to "/pet" with body') def step_impl(context): raise NotImplementedError(u'STEP: When I make POST request to "/pet" with body') @then(u'Response is 200') def step_impl(context): raise NotImplementedError(u'STEP: Then Response is 200')
Давайте это исправим:
features/steps/pet.py
import json import requests from behave import step ROOT_URL = r"https://petstore.swagger.io/v2" @step('I make POST request to "{path}" with body') def make_post_request_step(context, path): body = json.loads(context.text or "{}") url = ROOT_URL + path context.response = requests.post(url, json=body) @step("Response is {status_code:d}") def assert_response_step(context, status_code): assert ( context.response.status_code == status_code, f"Expected {status_code}, got {context.response.status_code}" )
Попробуем запустить ещё раз:
Feature: Everything about your Pets # features/pet.feature:1 As user I want to add, update, and delete pets in the pet store Scenario: I successfully create new empty pet # features/pet.feature:4 When I make POST request to "/pet" with body # features/steps/pet.py:9 Then Response is 200 # features/steps/pet.py:17 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 2 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.927s
Удостоверимся, что тест работает, немного поломав его. Пусть мы будем ожидать ответ 300 вместо 200:
Feature: Everything about your Pets # features/pet.feature:1 As user I want to add, update, and delete pets in the pet store Scenario: I successfully create new empty pet # features/pet.feature:4 When I make POST request to "/pet" with body # features/steps/pet.py:9 Then Response is 300 # features/steps/pet.py:17 Assertion Failed: Expected 300, got 200 Failing scenarios: features/pet.feature:4 I successfully create new empty pet 0 features passed, 1 failed, 0 skipped 0 scenarios passed, 1 failed, 0 skipped 1 step passed, 1 failed, 0 skipped, 0 undefined Took 0m1.026s
Замечательно! Теперь при помощи этих шагов можно написать и негативные сценарии:
features/pet.feature
Scenario: I get an error if I try to create a pet with invalid category id When I make POST request to "/pet" with body """ { "category": {"id": "zero", "name": "cats"} } """ Then Response is 500 Scenario: I get an error if I try to create a pet with invalid tag id When I make POST request to "/pet" with body """ { "tags": {"id": "zero", "name": "puffy"} } """ Then Response is 500
Всё это здорово, но довольно-таки простовато. Всё же мы бы хотели как-то проверять сам контент возвращаемого JSON-а. Давайте добавим новый шаг, в котором мы бы могли сравнивать контент JSON-а с описанным в тесте.
feature/steps/pet.py
def has_json_response(context) -> bool: try: context.response.json() return True except ValueError or AttributeError: return False @step("Response contains json with items") def assert_response_contains_json(context): assert ( has_json_response(context), f"Expected json, but got {context.response.text}" ) actual_values = context.response.json() expected_values = json.loads(context.text) for field in expected_values: expected_value = expected_values[field] actual_value = actual_values[field] match expected_value: case "<{any_int}>": assert ( isinstance(actual_value, int), f'Expected "{field}" is a number value, got {actual_value}' ) case _: assert ( expected_value == actual_value, f'Expected "{field}" equals {expected_value}, got {actual_value}' )
Рассмотрев внимательнее реализацию этого степа, можно заметить, что он может делать две вещи:
проверка на равенство значений, если в качестве ожидаемого значения указать число
проверка на принадлежность значения множеству целых чисел, если в качестве ожидаемого значения указать строку
<{any_int}>
Такое поведение пригодится нам, потому что зачастую мы не можем знать наверняка, какое значение имеет то или иное поле, но нам достаточно проверить, что в таком поле содержится любое число.
Обновим первый тест:
features/pet.feature
Scenario: I successfully create new empty pet When I make POST request to "/pet" with body Then Response is 200 And Response contains json with items """ { "id": "<{any_int}>", "tags": [], "photoUrls": [] } """
Напоследок давайте попробуем написать параметризованный сценарий, в котором мы создадим нового питомца с заданными полями:
features/pet.feature
Scenario Outline: I successfully create new pet with specified name, category, status, tags and photo When I make POST request to "/pet" with body """ { "name": "Barsik", "category": {"name": "cats"}, "status": "<category>", "tags": [ {"name": "puffy"}, {"name": "young"} ] } """ Then Response is 200 And Response contains json with items """ { "id": "<{any_int}>", "name": "Barsik", "category": {"id": 0, "name": "cats"}, "status": "<category>", "tags": [ {"id": 0, "name": "puffy"}, {"id": 0, "name": "young"} ], "photoUrls": [] } """ Examples: | category | | available | | pending | | sold |
Полностью проект доступен по ссылке: github.com/rmksrv/swaggerhub-petstore-bdd
Заключение
Целью этой статьи было познакомить читателя с BDD методологией, и, поскольку это было знакомство, то мы прошлись только по верхам. Тем не менее этого вполне достаточно, чтобы использовать её в своих проектах. Основное преимущество данного подхода заключается в возможности написания тест��в на естественном языке, что делает понятным требуемое поведение системы. При вдумчивом подходе, в конце концов, можно получить такие тесты, которые были бы достаточно ясны и читабельны, чтобы их скинуть вашему бизнес-аналитику вместо документации. А в некоторых случаях и бизнес-аналитик сможет сам накидать пару тестовых сценариев.
Материал был написан в феврале 2024-го года на основании опыта, полученного во время работы в ООО «Аурига».
Надеюсь, что статья была интересной и благодарю за внимание!
