BDD test scenarios with pytest-bdd in Python. Use when: writing pytest-bdd feature files, creating step definitions with @given/@when/@then decorators, setting up conftest.py for BDD fixtures, configuring pytest.ini for feature paths, generating Gherkin scenarios for Python projects, or debugging pytest-bdd step mismatches.
.feature files for a Python projectconftest.py fixturespip install pytest pytest-bdd
tests/
├── conftest.py # Shared fixtures and step imports
├── features/
│ └── login.feature # Gherkin feature files
└── step_defs/
└── test_login_steps.py # Step definitions (must start with test_)
pytest-bdd discovers step definition files via pytest's normal collection — the file name must start with
test_.
Create tests/features/<feature>.feature:
Feature: User Login
As a registered user
I want to log in with my credentials
So that I can access my account
Scenario: Successful login with valid credentials
Given the login page is displayed
When the user submits username "[email protected]" and password "secret"
Then the user is redirected to the dashboard
Scenario: Login fails with incorrect password
Given the login page is displayed
When the user submits username "[email protected]" and password "wrong"
Then an error message "Invalid credentials" is displayed
Create tests/step_defs/test_<feature>_steps.py:
import pytest
from pytest_bdd import given, when, then, parsers, scenarios
# Bind all scenarios from the feature file to this module
scenarios("../features/login.feature")
@given("the login page is displayed")
def login_page(page): # 'page' is a fixture from conftest.py
page.goto("/login")
@when(parsers.parse('the user submits username "{username}" and password "{password}"'))
def submit_login(page, username, password):
page.fill("[name=username]", username)
page.fill("[name=password]", password)
page.click("[type=submit]")
@then("the user is redirected to the dashboard")
def redirected_to_dashboard(page):
assert page.url.endswith("/dashboard")
@then(parsers.parse('an error message "{message}" is displayed'))
def error_message_displayed(page, message):
assert page.locator(".error").inner_text() == message
Parser options for step parameters:
| Parser | Usage | Example |
|---|---|---|
parsers.parse | {name} placeholder | parsers.parse('user "{name}"') |
parsers.cfparse | {name:Type} with type coercion | parsers.cfparse('age {n:d}') |
parsers.re | Full regex | parsers.re(r'amount (\d+)') |
conftest.py provides shared fixtures available to all step definition files:
import pytest
@pytest.fixture
def app_client():
"""Return a test client for the application."""
from myapp import create_app
app = create_app({"TESTING": True})
with app.test_client() as client:
yield client
@pytest.fixture
def authenticated_user(app_client):
"""Log in a default test user."""
app_client.post("/login", data={"username": "alice", "password": "secret"})
return app_client
In pytest.ini or pyproject.toml:
pytest.ini:
[pytest]
bdd_features_base_dir = tests/features
pyproject.toml:
[tool.pytest.ini_options]
bdd_features_base_dir = "tests/features"
With bdd_features_base_dir set, use relative paths in scenarios():
scenarios("login.feature") # resolved from bdd_features_base_dir
Scenario Outline: Login fails with invalid email format
Given the login page is displayed
When the user submits username "<email>" and password "secret"
Then a validation error "<error>" is displayed
Examples:
| email | error |
| notanemail | Please enter a valid email |
| @missing.com | Please enter a valid email |
pytest-bdd automatically parametrizes — no extra code needed in step defs.
# Run all BDD tests
pytest tests/step_defs/
# Run a specific feature
pytest tests/step_defs/test_login_steps.py
# Run with verbose Gherkin output
pytest tests/step_defs/ -v
# Run scenarios matching a tag (requires pytest-mark mapping)
pytest -m smoke
To map Gherkin tags to pytest marks, add to conftest.py:
# conftest.py
import pytest
from pytest_bdd import given # noqa: F401 — ensure registration
def pytest_bdd_before_scenario(request, feature, scenario):
for tag in scenario.tags:
request.applymarker(pytest.mark.smoke if tag == "smoke" else pytest.mark.skipif(False, reason=""))
Or use pytest_configure to register marks in conftest.py:
def pytest_configure(config):
config.addinivalue_line("markers", "smoke: critical path scenarios")
config.addinivalue_line("markers", "regression: full regression suite")
bdd_features_base_dir settingtest_scenarios() is called to bind scenarios to the test moduleconftest.py, not hardcoded in step defsScenario Outline + Examples tableparsers.parse used for steps with variable data