Behavior-Driven Development (BDD) in Python

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