How to bridge the gap between product engineers, QA testers, and developers — to make sure you’re speaking the same language?
The key lies in Behavior-Driven-Development (BDD) — given a feature, you test its behavior.
pytest-bdd
is a Pytest plugin that combines the flexibility and power of Pytest with the readability and clarity of BDD.
What is Behavior Driven Development (BDD)?
So what exactly is BDD?
To put it into simple terms, in software, behavior is how a feature or functionality works.
It’s a combination of inputs, actions, and outcomes.
For example — you log into a website, post a picture, see the picture on your timeline, fill out a form, submit it, receive an email, perform a search, get results, and so on.
All of these are user behaviors. They represent how a user interacts with a software system. Btw these could be interactions with a service API too.
Behavior Driven Development is the practice of software development that puts user behavior at the epicenter of software testing.
At its core, BDD encourages the creation of simple, understandable descriptions of software behavior as scenarios.
You start by identifying the desired outcomes of your features from the perspective of your end-users.
This involves writing user stories that describe how a feature will work from the user’s viewpoint.
Each story is then broken down into scenarios, which are further detailed using a format known as Gherkin.
The Gherkin Language
Gherkin is a domain-specific language designed for behavior descriptions, enabling all stakeholders to understand the behavior without needing technical knowledge.
The syntax is simple, focusing on the use of Given-When-Then statements to describe software features, scenarios, and outcomes.
Feature: Account Withdrawal
Scenario: Withdraw money from an account with sufficient funds
Given the account balance is $100
When the account holder requests $20
Then the account balance should be $80
Another one
Feature: User login
Scenario: Successful login with correct credentials
Given I am on the login page
When I enter valid credentials
Then I should be redirected to the dashboard
Pytest BDD Test Frameworks
Some popular frameworks for Pytest are pytest-bdd, behave and radish.
While all of these are popular, pytest-bdd
is one of the simplest and most commonly used.
Pytest Fixtures (Short Recap)
Pytest fixtures are a powerful feature for setting up and tearing down conditions for tests.
In the short example above, you can see how we can reuse the setup_database
fixture in several tests.
Writing Your First BDD Test with PyTest
Now let’s look at how to write your first BDD test.
Create a Feature File
Create a folder features
in your tests
folder.
Now create a file bank_transactions.feature
with the following.
Feature: Bank Transactions
Tests related to banking Transactions
Scenario: Deposit into Account
Given the account balance is $100
When I deposit $20
Then the account balance should be $120
The feature under test is “Bank Transactions”. The line below it is interpreted as comments.
The scenario is “Deposit into Account”.
-
Given an initial balance of $100
-
When $20 is deposited
-
The new balance should be $120
Core:
- Scenario — A Test Case or Behavior Specification
- Given — Initial State
- When — Takes an action
- Then — Verify an expected outcome
Write Step Definitions
Create a step_definitions
folder under tests
and include the following code.
tests/step_definitions/test_bank_transactions.py
import pytest
from pytest_bdd import scenarios, scenario, given, when, then
# Load all scenarios from the feature file
scenarios("../features/bank_transactions.feature")
# Fixtures
@pytest.fixture
def account_balance():
return {"balance": 100} # Using a dictionary to allow modifications
# Given Steps
@given("the account balance is $100")
def account_initial_balance(account_balance):
account_balance["balance"] = 100
# When Steps
@when("I deposit $20")
def deposit(account_balance):
account_balance["balance"] += 20
# Then Steps
@then("the account balance should be $120")
def account_balance_should_be(account_balance):
assert account_balance["balance"] == 120
Now let’s run it.
$ pytest tests/step_definitions/test_bank_transactions.py -v -s
Example 2 (Parameterization Features)
Feature: Bank Transactions
Tests related to banking Transactions
Scenario: Deposit into Account
Given the account balance is $"100"
When I deposit $"20"
Then the account balance should be $"120"
Scenario: Withdraw from Account
Given the account balance is $"100"
When I withdraw $"20"
Then the account balance should be $"80"
Example 3 (Scenario Outlines)
If you’re familiar with Pytest Parametrization, you know the power of generating multiple test case scenarios without writing n
test cases.
Feature: Bank Transactions
Tests related to banking Transactions
Scenario Outline: Deposit into Account
Given the account balance is $<balance>
When I deposit $<deposit>
Then the account balance should be $<new_balance>
Examples:
| balance | deposit | new_balance |
| 100 | 20 | 120 |
| 0 | 5 | 5 |
| 100 | 0 | 100 |
Using Tags
A typical production project may have hundreds if not thousands of scenarios.
Simple, by filtering, through tags.
Similar to Pytest markers, you can tag your features and scenarios using the @
symbol. Like this.
@banking
Feature: Bank Transactions
Tests related to banking Transactions
@deposit
Scenario: Deposit into Account
Given the account balance is $"100"
When I deposit $"20"
Then the account balance should be $"120"
@withdrawal
Scenario: Withdraw from Account
Given the account balance is $"100"
When I withdraw $"20"
Then the account balance should be $"80"
You can see we’ve applied the @banking
tag to the feature and individual @deposit
and @withdrawal
tags to the scenarios.
pytest -k "deposit