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.

EXAMPLE CODE REPO

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