| name | behave |
| description | [Applies to: **/*.py] Definitive guidelines for writing maintainable, readable, and effective BDD tests with behave in Python, emphasizing thin steps, clear Gherkin, and robust fixture management. |
| source | cursor_mdc |
behave Best Practices
This guide outlines the definitive best practices for writing behave tests. Adhere to these standards for consistent, high-quality BDD automation.
1. Code Organization & Structure
Maintain a clear, predictable project structure.
✅ GOOD: Standard Project Layout
.
├── features/
│ ├── authentication.feature # Gherkin feature files
│ ├── product_management.feature
│ ├── environment.py # Global hooks and fixtures
│ └── steps/
│ ├── common_steps.py # Reusable, generic steps
│ ├── auth_steps.py # Authentication-specific steps
│ └── product_steps.py # Product-specific steps
├── src/ # Your application code
│ └── my_app/
│ ├── __init__.py
│ ├── models.py
│ └── services.py # Business logic layer
└── pyproject.toml # Project configuration
2. Feature Files: Clear, Business-Centric Gherkin
Feature files describe what the system does from a business perspective, not how it's implemented. Keep scenarios concise.
❌ BAD: Implementation Details in Gherkin
Feature: User Login
Scenario: Successful login
Given I am on the "/login" page
When I fill in "username" with "testuser" and "password" with "password123"
And I click the "Login" button with CSS selector ".login-form button"
Then I should be redirected to "/dashboard"
✅ GOOD: Abstract, User-Focused Gherkin
Feature: User Login
Scenario: Successful login with valid credentials
Given a registered user "testuser" with password "password123"
When "testuser" logs in
Then "testuser" should be logged in successfully
3. Step Implementations: Thin & Pythonic
Steps should be thin wrappers that delegate complex logic to your application's service layer or dedicated helper modules. Follow PEP 8.
❌ BAD: Fat Steps with Business Logic
from behave import given
from my_app.models import User
@given('a registered user "{username}" with password "{password}"')
def step_impl(context, username, password):
user = User.objects.create_user(username=username, password=password)
context.user = user
✅ GOOD: Thin Steps Delegating to Service Layer
from behave import given, when, then
from my_app.services import user_service
@given('a registered user "{username}" with password "{password}"')
def step_impl(context, username: str, password: str):
context.user = user_service.create_user(username, password)
@when('"{username}" logs in')
def step_impl(context, username: str):
context.response = user_service.login_user(username, context.user.password)
@then('"{username}" should be logged in successfully')
def step_impl(context, username: str):
assert context.response.status_code == 200
assert user_service.is_logged_in(username)
4. Context Object: State Management
Use the context object to share state between steps within a scenario. Avoid polluting it with unnecessary or complex data structures.
✅ GOOD: Minimal & Typed Context Usage
from behave import given, when, then
@given('a product with name "{name}" and price {price:f}')
def step_impl(context, name: str, price: float):
context.product_name = name
context.product_price = price
@when('I add the product to the cart')
def step_impl(context):
pass
5. Fixtures & Hooks: environment.py
Manage setup and teardown declaratively in features/environment.py. Use behave's built-in fixture system.
✅ GOOD: Centralized Fixture Management
from behave.fixture import use_fixture_by_tag
from features.fixtures import browser_session, database_cleanup
def before_all(context):
context.config.setup_logging()
def before_feature(context, feature):
pass
def before_scenario(context, scenario):
use_fixture_by_tag(context, scenario.tags, "browser.session", browser_session)
use_fixture_by_tag(context, scenario.tags, "database.cleanup", database_cleanup)
def after_scenario(context, scenario):
pass
def after_all(context):
pass
from behave import fixture
@fixture
def browser_session(context):
context.browser = "some_selenium_browser_instance"
yield context.browser
context.browser.quit()
():
context.db_transaction =
context.db_transaction.rollback()
Gherkin with Tags to trigger fixtures:
@browser.session @database.cleanup
Feature: User Interface Interaction
Scenario: Verify homepage content
Given I open the browser
When I navigate to the homepage
Then I should see "Welcome"
6. Tagging for Test Management
Use tags (@tag_name) to categorize scenarios for selective execution (e.g., @smoke, @regression, @slow).
✅ GOOD: Granular Tagging
@smoke @authentication
Feature: User Login
Scenario: Successful login
# ...
@slow @integration @database_heavy
Feature: Order Processing
Scenario: Complete order workflow
# ...
Running Tests with Tags:
behave -t @smoke
behave -t "@integration and not @slow"
7. Mocking Strategies
Mock external dependencies at the service layer, not directly within steps. Use unittest.mock for controlled isolation.
✅ GOOD: Mocking at Service Layer
import requests
def get_external_data(item_id: str) -> dict:
response = requests.get(f"https://api.external.com/items/{item_id}")
response.raise_for_status()
return response.json()
from behave import when, then
from unittest.mock import patch
from my_app.services import get_external_data
@when('I request external data for "{item_id}"')
def step_impl(context, item_id: str):
with patch('my_app.services.requests.get') as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"id": item_id, "data": "mocked"}
context.external_data = get_external_data(item_id)
@then('the external data should be mocked')
def step_impl(context, item_id: str):
assert context.external_data["data"] == "mocked"
8. Type Hints
Apply type hints to step function arguments and helper functions for improved readability, maintainability, and static analysis.
✅ GOOD: Explicit Type Hints
from behave import given, when, then
@given('a number {num:d}')
def step_impl(context, num: int):
context.number = num
@when('I add {value:d} to the number')
def step_impl(context, value: int):
context.number += value
@then('the number should be {expected:d}')
def step_impl(context, expected: int):
assert context.number == expected
9. Coverage Patterns
Focus code coverage on your application's business logic, not the behave step definitions themselves.
✅ GOOD: Measure Application Code Coverage
Configure coverage.py (or pytest-cov if integrating with pytest) to include your src/ directory and explicitly exclude features/ and features/steps/.
[run]
source = src/my_app/
omit =
features/*
features/steps/*
10. Performance Considerations
Optimize for speed by isolating slow tests and using efficient setup/teardown.
✅ GOOD: Isolate Slow Tests with Tags
@slow @database_heavy
Feature: Complex Reporting
Scenario: Generate large report
# ... This scenario takes a significant amount of time
Run slow tests separately: behave -t @slow
✅ GOOD: Efficient Fixtures
Ensure before_scenario and after_scenario hooks are as fast as possible. Use before_all for truly global, one-time setup.
import some_db_module
def before_all(context):
context.db_connection_pool = some_db_module.create_connection_pool()
def after_all(context):
context.db_connection_pool.close_all()
def before_scenario(context, scenario):
context.db_session = context.db_connection_pool.get_session()
def after_scenario(context, scenario):
context.db_session.close()