Behavior-Driven Development: Python with Pytest BDD

This guide covers essential topics of implementing BDD in Python projects using the Pytest BDD framework, such as setting up the testing environment, utilizing fixtures and tags for efficient test management, and best practices for writing maintainable BDD tests. It also discusses the advantages and potential drawbacks of using Pytest-BDD, offering insights to help teams effectively integrate BDD into their development workflow.

9 min read
568 views

If you want your IT projects to grow, your technical teams and stakeholders without tech backgrounds do not suffer from misunderstandings during the software development process. You can use the BDD framework to connect the team on one page and keep everyone aligned.

In the article, you can discover more information about the Pytest BDD framework, learn how to write BDD tests with Pytest, and reveal some considerations to help you make the most of the Pytest BDD test framework.

Why teams need the Pytest BDD framework

If your team works on Python projects, pytest-BDD will give them a sizable boost in project clarity.

  • Tech teams and non-technical business executives can take part in writing test scenarios with Gherkin syntax to describe the intended behavior of software in a readable format to make sure it meets business requirements.
  • Teams can verify user stories and system behavior by directly linking them to feature requirements.
  • Teams can make the test automation process more scalable with pytest’s features like fixtures and plugins.
  • Teams can create a solid Steps base for test cases and reuse code in other tests by turning scenarios into automated tests.
  • Teams can easily update BDD scenarios as the product changes.
  • Teams can get detailed test reports with relevant information about the testing efforts.

Fixtures & Tags: Why use them?

With pytest-bdd, teams can use the power of the entire Pytest ecosystem, such as fixtures and tags.

Fixtures

Marked with the @pytest.fixture decorator, fixtures are known as special functions that provide a way to set up and tear down resources required for your tests. They are very flexible and have multiple use cases – applied to individual tests, entire test classes, or even across a whole test session to optimize resource usage. There are various reasons for using Pytest fixtures:

Fixtures are implemented in a modular manner and are easy to use.
Fixtures have a scope (function, class, module, session) and lifetime that help to define how often they are created and destroyed, which is crucial for efficient and reliable testing.
Fixtures with function scope improve the test code’s readability and consistency to simplify the code maintenance process.
Pytest fixtures allows testing complex scenarios, sometimes carrying out the simple.
Fixtures use dependency injection (configuration settings, database connections, external services) to improve test readability and maintainability by encapsulating setup and teardown logic.

While fixtures are great for extracting data or objects that you use across multiple tests, you may not use them for tests that require slight variations in the data.

Tags

Tags are a powerful feature that helps selectively run certain tests based on their labels. They also allow teams to assign tags to scenarios in feature files and use pytest to execute tests, especially when dealing with large test suites. Tags can be used to indicate test priority, skip certain tests under specific conditions, or group tests by categories like performance, integration, or acceptance. Let’s consider the reasons for using tags:

You need to run suites of tests that are relevant to your current needs, like testing a particular feature.
You need to group tests based on their functionality, priority, or other relevant criteria to easily understand the test suite structure and find specific tests in the future.
You need to execute tests that match multiple tags by using logical operators (AND, OR, NOT) to precisely target the tests you want to run.
You need to automate the execution of specific test subsets and get customized reports based on test tags.

While tags help categorize the tests based on any criteria, their overuse can lead to a cluttered test suite and make it hard for developers to understand or maintain the code.

In fact, Pytest has limitations, but it comes with many plugins that extend its capabilities, among them the Python BDD plugin, which we are interested in at this point in the article. This plugin provides all the advantages of Python in BDD, which is why many automators love it ❤️

Getting Started with Pytest BDD

Prerequisites: Setting up the environment

If you are ready to utilize pytest-BDD, you need to make sure that all the required tools and libraries are installed. Below you can find out the steps to follow to set up the environment and start writing BDD tests:

    1. Install Python. You need to download the latest version from Python’s official website to get Python installed on your system. Then you need to verify the installation by running the command:
      python --version
    2. I used the optional alias python to python3 (macOS/Linux only) because I saw messages: command not found, as python3 was installed instead of python.
      alias python=python3
      
    3. I installed the package manager pip.
    4. Indeed, some test automation engineers prefer to use the Poetry library over Virtualenv. Poetry is more modern and enables management of dependencies in the global project directory without manually activating environments.
    5. Set up a Virtual Environment. At this step, you can create a virtual environment for your project to isolate it from other environments, give you full control of your project, and make it easily reproducible. Firstly, you need to install the virtualenv package if you haven’t already with pip. Once installed, you can specify the Python version and the desired name for the environment. It is a good practice to replace <version> with your Python version and <virtual-environment-name> with the environment name you want to give.
      pip install virtualenv
    6. Install pytest and pytest-BDD. At this step, you can use pip to install both the pytest framework and the Pytest-BDD plugin
      pip install pytest pytest-bdd
    7. Install Additional Dependencies. If you need additional libraries like Selenium or Playwright, you can install them as well. We need them to operate on a browser. For instance Playwright
      playwright install
      
    8. Activate virtualenv based on your OS
      source venv/bin/activate
      
    9. Create Feature Files and Steps File. The last step before writing the BDD tests is creating a structured project directory where you will keep your feature files and test scripts. Typical project structure looks like:
      pytest_bdd_selenium/
      ├── features/
      │   └── login.feature
      ├── steps/
      │   └── test_login_steps.py
      ├── tests/
      │   └── test_login.py
      ├── conftest.py
      ├── requirements.txt
      └── pytest.ini
      

How to write BDD Tests with Pytest

To write a BDD Test with Pytest, as mentioned above, you need to create a feature file and define step functions that match the scenarios in the feature file.

#1: Writing Feature File

To write feature files, you need to understand the Gherkin syntax used to describe the behavior of the application in plain English. The “given/when/then” vocabulary is pretty clear to all team members – analysts, developers, testers, and other specialists without technical background. Generally, the feature files work as living documentation of the system’s expected behavior. More information about Gherkin-based feature files can be found here.

Here is a basic example of a successful login functionality on this site https://practicetestautomation.com/practice-test-login/ 

Feature: Login functionality

  Scenario: Successful login with valid credentials
    Given the user is on the login page
    When the user enters valid username and password
    Then the user should see the secure area

#2: Creating Step Definitions

Step Definitions map the Gherkin steps in your feature files to Python functions. Pytest-bdd matches the steps in feature files with corresponding step definitions. Here is an example code for user login:

from pytest_bdd import scenarios, given, when, then

scenarios('../features/login.feature')

LOGIN_URL = "https://practicetestautomation.com/practice-test-login/"
USERNAME = "student"
PASSWORD = "Password123"

@given("the user is on the login page")
def open_login_page(browser_context):
    browser_context.goto(LOGIN_URL)

@when("the user enters valid username and password")
def login_user(browser_context):
    browser_context.fill("#username", USERNAME)
    browser_context.fill("#password", PASSWORD)
    browser_context.click("#submit")

@then("the user should see the secure area")
def check_login(browser_context):
    header = browser_context.locator("h1")
    assert "Logged In Successfully" in header.text_content()

* File test_login.py might be empty if all scenarios are loaded from a step file.

#3: Create Conftest file

Now, Playwright uses built-in fixtures like Page, and in many cases, we do not need it — Playwright provides everything.

You only need it if you want to:

  • Add custom fixtures (e.g., for login tokens, DB, API)
  • Change browser settings (e.g., headless, slow motion)
  • Set up project-wide hooks
  • Configure Playwright launch options

Our basic application is a login, so we have to create conftest.py

import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture
def browser_context():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)  # set True for headless
        context = browser.new_context()
        page = context.new_page()
        yield page
        browser.close()

Using a Page, you can update your steps/test_login_steps.py file

from pytest_bdd import scenarios, given, when, then
from playwright.sync_api import Page

scenarios('../features/login.feature')

LOGIN_URL = "https://practicetestautomation.com/practice-test-login/"
USERNAME = "student"
PASSWORD = "Password123"

@given("the user is on the login page")
def open_login_page(page: Page):
    page.goto(LOGIN_URL)

@when("the user enters valid username and password")
def login_user(page: Page):
    page.fill("#username", USERNAME)
    page.fill("#password", PASSWORD)
    page.click("#submit")

@then("the user should see the secure area")
def check_login(page: Page):
    assert "Logged In Successfully" in page.text_content("h1")

#3: Executing PyTest BDD

Once the feature file and step definitions have been created, you can start test execution. It can be done with the pytest command:

pytest -v

#5: Analizing Results

After PyTest BDD tests execution, you can analize, measure and review your testing efforts to identify weaknesses and formulate solutions that improve the process in the future.

Playwright BDD PyTest Reporting screen
Playwright BDD PyTest Reporting

If you integrate pytest BDD with a test case management system such as testomat.io, you can generate test reports, analyze them, and get the picture of how your tested software performs. 

Playwright Trace Viewer feature test management
Playwright Trace Viewer

You can debug your Playwright tests right inside the test management system for faster troubleshooting and smoother test development.

Advantages of Pytest BDD

  • Pytest BDD works flawlessly with Pytest and all major Pytest plugins.
  • With the fixtures feature, you can manage context between steps.
  • With conftest.py, you can share step definitions and hooks.
  • You can execute filtered tests alongside other Pytest tests.
  • When dealing with functions that accept multiple input parameters, you can use tabular data to run the same test function with different sets of input data and make tests maintainable.

Disadvantages of Pytest BDD

  • Step definition modules must have explicit declarations for feature files (via @scenario or the “scenarios” function).
  • Scenario outline steps must be parsed differently
  • Sharing steps between feature files can be a bit of a challenge.

Rules to follow when using Pytest BDD for Test Automation

Below you can find some important considerations when using Pytest-bdd:

  • ‍You need to utilize Gherkin syntax with GWT statements.
  • You need to use steps as Python functions so that pytest-bdd can match them in attribute files with their corresponding step definitions to be parameterized or defined as regular Python functions.
  • You need to utilize the pytest-bdd and pytest fixture together to set up and break down the environment for testing.
  • Each scenario works as an individual test case. You need to run the BDD test using the standard pytest command.
  • You can use pytest-bdd hooks to generate code before or after events in the BDD test lifecycle.
  • You can use tags to run specific groups of tests, prioritize them, or group them by functionality.

Bottom Line: Ready to use Pytest BDD for Python project?

With pytest-BDD, your teams get a powerful framework to implement BDD in Python projects. When writing tests in a clear and Gehrkin-readable format, teams with different backgrounds can better collaborate, understand business requirements, and build what the business really needs. Contact us if you need more information about improving your Pytest BDD workflow and its integration with the testomat.io test case management system and increasing test coverage.

📋 Test management system for Automated tests
Manage automation testing along with manual testing in one workspace. ⚙️
Follow us