
Behavior-Driven Development (BDD) in Python
Behavior-Driven Development, or BDD, is an approach to software development that encourages collaboration between developers, QA, and non-technical participants in a software project. It does this by using concrete examples to illustrate how the application should behave. If you’ve ever been in a situation where requirements were ambiguous or misinterpreted, BDD is here to help. At its heart, BDD is about writing tests in a language that everyone can understand — not just programmers.
BDD builds on the principles of Test-Driven Development (TDD) but shifts the focus from “testing” to “specifying behavior.” Instead of writing unit tests that check functions in isolation, you describe features and scenarios in plain language. These scenarios then become executable specifications. In Python, one of the most popular tools for implementing BDD is behave
.
Let’s start by installing behave
. You can do this easily using pip:
pip install behave
Once installed, you’re ready to structure your project for BDD. A typical behave
project has a features
directory containing .feature
files (where you write your scenarios in Gherkin syntax) and a steps
directory where you implement the step definitions in Python.
Gherkin is a simple, structured language that uses keywords like Feature, Scenario, Given, When, and Then. Here’s a basic example of a .feature
file:
Feature: User login
As a user
I want to login to the system
So that I can access my personal dashboard
Scenario: Successful login with valid credentials
Given the user is on the login page
When the user enters valid username and password
And clicks the login button
Then the user should be redirected to the dashboard
Each line in the scenario is a step. Now, we need to write step definitions — Python functions that tell behave
what to do when it encounters each step.
Create a Python file in the steps
directory, for example login_steps.py
:
from behave import given, when, then
@given('the user is on the login page')
def step_impl(context):
context.browser.get('http://localhost:5000/login')
@when('the user enters valid username and password')
def step_impl(context):
context.browser.find_element_by_id('username').send_keys('testuser')
context.browser.find_element_by_id('password').send_keys('password123')
@when('clicks the login button')
def step_impl(context):
context.browser.find_element_by_id('login-btn').click()
@then('the user should be redirected to the dashboard')
def step_impl(context):
assert 'dashboard' in context.browser.current_url
This example uses a hypothetical web browser (like Selenium) via a context
object, which is shared across steps. You can use context
to store state between steps, which is very useful.
Now, run your tests with:
behave
behave
will parse your .feature
files, match each step to its corresponding Python function, and execute them in order.
But BDD isn’t just for web applications. You can use it for any kind of software — APIs, command-line tools, libraries, etc. Let’s look at a simpler example without a browser, testing a function directly.
Suppose we have a function that calculates the sum of a list of numbers. First, write the feature:
Feature: Sum calculation
In order to avoid manual calculation mistakes
As a user
I want the software to sum a list of numbers for me
Scenario: Sum a list of positive numbers
Given I have a list of numbers [1, 2, 3]
When I calculate the sum
Then the result should be 6
Now, implement the steps:
from behave import given, when, then
@given('I have a list of numbers [{numbers}]')
def step_impl(context, numbers):
context.numbers = [int(n) for n in numbers.split(',')]
@when('I calculate the sum')
def step_impl(context):
context.result = sum(context.numbers)
@then('the result should be {expected:d}')
def step_impl(context, expected):
assert context.result == expected
Notice how we used {numbers} and {expected:d} in the step patterns. These are parse expressions that extract values from the step text and pass them as arguments to the step function. This makes your steps reusable with different data.
You can also use tables in your scenarios for more complex data. For example:
Scenario: Sum multiple lists of numbers
Given I have the following numbers:
| numbers |
| 1,2,3 |
| 4,5 |
When I calculate the sum for each
Then the results should be:
| result |
| 6 |
| 9 |
And the step definitions can handle the table:
@given('I have the following numbers:')
def step_impl(context):
context.number_lists = []
for row in context.table:
nums = [int(n) for n in row['numbers'].split(',')]
context.number_lists.append(nums)
@when('I calculate the sum for each')
def step_impl(context):
context.results = [sum(nums) for nums in context.number_lists]
@then('the results should be:')
def step_impl(context):
expected = [int(row['result']) for row in context.table]
assert context.results == expected
This is powerful because you can run the same scenario with different data sets without changing the step code.
Another useful feature of behave
is backgrounds. A background lets you define steps that run before each scenario in a feature. This is great for setup steps that are common to all scenarios. For example:
Feature: Shopping cart
Background:
Given I am logged in as a registered user
And I have an empty shopping cart
Scenario: Add item to cart
When I add a "Python Book" to the cart
Then the cart should contain 1 item
Scenario: Remove item from cart
Given the cart contains a "Python Book"
When I remove the "Python Book" from the cart
Then the cart should be empty
The background steps run before each scenario, so you don’t have to repeat the login and empty cart setup.
You can also use tags to organize your features and scenarios. Tags start with “@” and can be used to run only specific tests. For example:
@web @login
Feature: User login
# scenarios here
@api @fast
Feature: API authentication
# scenarios here
Run only scenarios with a specific tag:
behave --tags=web
This runs all scenarios tagged with @web
.
Now, let’s talk about some best practices for BDD in Python:
- Write scenarios from the user’s perspective. Use language that non-developers can understand.
- Keep your scenarios short and focused. Each should test one specific behavior.
- Avoid too many details in steps. Use tables or parse expressions for data.
- Reuse step definitions whenever possible to reduce duplication.
- Use the
context
object wisely to share state between steps, but avoid making it too large or messy.
BDD does require an initial investment in time and collaboration, but the payoff is huge: clearer requirements, fewer bugs, and better alignment between technical and non-technical team members.
If you’re working on a team, consider involving product owners or business analysts in writing the .feature
files. This ensures that the examples accurately reflect desired behavior.
For larger projects, you might want to integrate behave
with other tools. For example, you can generate pretty reports with:
behave -f html -o report.html
This creates an HTML report showing which scenarios passed or failed.
You can also use behave
with continuous integration systems like Jenkins or GitHub Actions to run your BDD tests automatically on every commit.
Here’s a comparison of popular BDD frameworks in Python:
Framework | Pros | Cons |
---|---|---|
behave | Simple, uses Gherkin, popular | Less built-in than some others |
pytest-bdd | Integrates with pytest | Steeper learning curve |
lettuce | Lightweight, easy to start | Less actively maintained |
While behave
is a great choice, you might also want to check out pytest-bdd
if you’re already using pytest
for your testing.
Remember, the goal of BDD is not just to write tests but to improve communication and ensure everyone has a shared understanding of what the software should do. Start small, practice with a few features, and gradually adopt BDD in your projects.
If you get stuck, the behave
documentation is excellent, and there’s a large community around BDD. Happy testing