Using Given-When-Then for Readable Python Tests
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].
- Readability: The test becomes a piece of documentation that explains its own purpose without requiring the reader to parse the code's logic.
- 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]. - 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
- "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
- "Given-When-Then: A Guide for Python Testing." Medium.
https://medium.com/@george_t/given-when-then-a-guide-for-python-testing-806734c311c9
- "Behavior-Driven Development (BDD)." ThoughtWorks.
https://www.thoughtworks.com/insights/blog/given-when-then-bdd
- "Writing a Given-When-Then Test in PyTest." Better Programming.
https://betterprogramming.pub/given-when-then-pytest-96e00b5f137e
- "A Pythonic approach to BDD." Gherkin.
https://gherkin.readthedocs.io/en/latest/