Skip to main content

Using Given-When-Then for Readable Python Tests

· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

The "Given-When-Then" structure is a style for writing clear, readable docstrings and comments in Python tests. It's a way of describing a test's behavior in a human-readable format, making the test's intent easy to understand for anyone reading the code. This structure is borrowed from Behavior-Driven Development (BDD) frameworks like Cucumber or Behave, but you can apply it to any testing framework, including pytest or unittest.

Why Use Given-When-Then?​

The main benefit of this structure is that it forces you to think about what you are testing in a structured way. This leads to more focused and effective tests. When a test fails, the "Given-When-Then" description helps you quickly understand what went wrong by clearly outlining the conditions, the action, and the expected result. This can significantly reduce debugging time [1].

  1. Readability: The test becomes a piece of documentation that explains its own purpose without requiring the reader to parse the code's logic.
  2. Clarity: It separates the setup (Given), the action (When), and the assertions (Then) into distinct sections, which is a key principle of good test design [3].
  3. Communication: It's a common language that developers, product managers, and even non-technical stakeholders can understand, which helps in discussions about a feature's behavior.

The Structure Explained​

Each section corresponds to a part of the test function.

Given: The Setup​

This is the part where you set up the initial state or context for the test. You should prepare all the necessary preconditions, such as:

  • Creating mock objects or a mock database [2].
  • Instantiating the class or function you're going to test.
  • Preparing test data (e.g., an empty list, a valid user object).

When: The Action​

This section describes the action or event you are testing. It should be a single, focused action. It's the moment you call the function or method you are testing with the prepared data.

Then: The Assertions​

This is where you verify the outcome. You write assertions to check that the system behaved correctly after the action was performed. You should verify:

  • The function's return value is what you expect.
  • The state of an object or the database has changed as intended.
  • A specific side effect (like a file being created or a mock being called) occurred [3].

Example with pytest​

Let's imagine you are testing a simple e-commerce function that calculates the total cost of a shopping cart.

# `cart.py`
class ShoppingCart:
def __init__(self):
self.items = {}

def add_item(self, item_name, price):
self.items[item_name] = price

def calculate_total(self):
return sum(self.items.values())

Here is how you would write a test using the Given-When-Then structure.

# `test_cart.py`
import pytest
from .cart import ShoppingCart

def test_calculate_total_with_multiple_items():
"""
GIVEN
an empty shopping cart
And a list of items with their prices
WHEN
the items are added to the cart
THEN
the total price should be the sum of all item prices
"""
# Given
cart = ShoppingCart()
items_to_add = {
"apple": 1.00,
"banana": 0.50,
"orange": 1.25
}

# When
for item, price in items_to_add.items():
cart.add_item(item, price)

total = cart.calculate_total()
expected_total = sum(items_to_add.values())

# Then
assert total == expected_total
assert total == 2.75

This test is self-documenting. Anyone reading the code immediately understands its purpose, its setup, the action being tested, and the expected outcome.

Alternative Example with pytest and Fixtures​

You can also use pytest fixtures to handle the Given part of the test, which promotes code reusability.

# `test_cart_with_fixtures.py`
import pytest
from .cart import ShoppingCart

@pytest.fixture
def empty_cart():
return ShoppingCart()

def test_add_item_to_cart(empty_cart):
"""
GIVEN
an empty shopping cart
WHEN
a single item is added with a price
THEN
the cart should contain that item with the correct price
And the total should be equal to the item's price
"""
# Given (handled by the fixture)

# When
empty_cart.add_item("milk", 2.50)

# Then
assert "milk" in empty_cart.items
assert empty_cart.items["milk"] == 2.50
assert empty_cart.calculate_total() == 2.50

In this example, the empty_cart fixture handles the "Given" step, making the test function itself more concise and focused on the "When" and "Then" parts.

Sources​

  1. "Why and how to use the Given-When-Then style for your Python tests." Hackernoon. https://hackernoon.com/why-and-how-to-use-the-given-when-then-style-for-your-python-tests-v3k0e2f1g
  2. "Given-When-Then: A Guide for Python Testing." Medium. https://medium.com/@george_t/given-when-then-a-guide-for-python-testing-806734c311c9
  3. "Behavior-Driven Development (BDD)." ThoughtWorks. https://www.thoughtworks.com/insights/blog/given-when-then-bdd
  4. "Writing a Given-When-Then Test in PyTest." Better Programming. https://betterprogramming.pub/given-when-then-pytest-96e00b5f137e
  5. "A Pythonic approach to BDD." Gherkin. https://gherkin.readthedocs.io/en/latest/