Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on living documentation in Python testing! ๐ Have you ever wondered how to keep your test documentation always up-to-date and accurate? Thatโs exactly what weโll explore today!
Living documentation is like having a smart assistant that automatically updates your documentation as your tests evolve. Instead of maintaining separate documents that quickly become outdated, your tests themselves become the documentation! ๐
By the end of this tutorial, youโll be creating self-documenting tests that serve as both quality assurance AND up-to-date documentation for your projects. Letโs dive in! ๐โโ๏ธ
๐ Understanding Living Documentation
๐ค What is Living Documentation?
Living documentation is like a garden that grows and changes with the seasons ๐ฑ. Unlike traditional documentation thatโs written once and forgotten, living documentation evolves automatically with your code!
In Python testing terms, living documentation means your tests serve multiple purposes:
- โจ They verify your code works correctly
- ๐ They document how to use your code
- ๐ก๏ธ They provide examples of expected behavior
๐ก Why Use Living Documentation?
Hereโs why developers love living documentation:
- Always Current ๐: Documentation updates automatically with code changes
- Executable Examples ๐ป: Documentation that runs and verifies itself
- Single Source of Truth ๐: No more sync issues between docs and code
- Better Test Names ๐ง: Forces clear, descriptive test naming
Real-world example: Imagine documenting an API ๐. With living documentation, your tests show exactly how each endpoint works, what data it expects, and what it returns - all verified by running tests!
๐ง Basic Syntax and Usage
๐ Simple Example with pytest
Letโs start with a friendly example using pytest:
# ๐ Hello, Living Documentation!
import pytest
class TestShoppingCart:
"""๐ Shopping Cart API Documentation
This test suite serves as living documentation
for our shopping cart functionality.
"""
def test_adding_item_to_cart_increases_total(self):
"""โ
Adding items to cart should update the total price
Example:
When a customer adds a $10 item to their cart,
the cart total should increase by $10.
"""
# ๐ Create a new shopping cart
cart = ShoppingCart()
# ๐ฎ Add a video game to the cart
cart.add_item("Super Python Bros", price=59.99)
# ๐ก The total should match the item price
assert cart.total == 59.99
def test_applying_discount_code_reduces_total(self):
"""๐๏ธ Discount codes should reduce the cart total
Example:
A 20% discount code on a $100 cart
should result in an $80 total.
"""
# ๐ Setup cart with items
cart = ShoppingCart()
cart.add_item("Python Cookbook", price=49.99)
cart.add_item("Testing with pytest", price=39.99)
# ๐ฏ Apply discount code
cart.apply_discount("SAVE20") # 20% off
# โจ Total should be reduced by 20%
expected_total = (49.99 + 39.99) * 0.8
assert cart.total == expected_total
๐ก Explanation: Notice how our test names and docstrings create readable documentation! When someone reads these tests, they understand exactly how the shopping cart works.
๐ฏ Using doctest for Inline Documentation
Pythonโs doctest module is perfect for living documentation:
# ๐๏ธ calculator.py - Self-documenting code!
def add_numbers(a, b):
"""Add two numbers together.
This function demonstrates living documentation
using doctest examples.
Examples:
>>> add_numbers(2, 3)
5
>>> add_numbers(10, 20)
30
>>> add_numbers(-5, 5)
0
>>> add_numbers(0.1, 0.2) # ๐ฏ Floating point!
0.30000000000000004
"""
return a + b
def calculate_discount(price, discount_percent):
"""Calculate the final price after applying a discount.
Args:
price: Original price (must be positive)
discount_percent: Discount percentage (0-100)
Examples:
>>> calculate_discount(100, 20)
80.0
>>> calculate_discount(50, 10)
45.0
>>> calculate_discount(29.99, 15)
25.4915
>>> calculate_discount(100, 0) # ๐ก No discount
100.0
>>> calculate_discount(100, 100) # ๐ Free!
0.0
"""
if price < 0:
raise ValueError("Price must be positive! ๐ฐ")
if not 0 <= discount_percent <= 100:
raise ValueError("Discount must be 0-100%! ๐๏ธ")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
๐ก Practical Examples
๐ Example 1: E-commerce Order Processing
Letโs build living documentation for an order system:
# ๐ฆ test_order_processing.py
import pytest
from datetime import datetime
from decimal import Decimal
class TestOrderProcessing:
"""๐ฆ Order Processing System Documentation
This suite documents our order processing workflow
through executable examples.
"""
@pytest.fixture
def sample_order(self):
"""๐๏ธ Create a sample order for testing"""
return Order(
customer_email="[email protected]",
items=[
{"name": "Python Mug โ", "price": 12.99, "qty": 2},
{"name": "Debug Duck ๐ฆ", "price": 9.99, "qty": 1}
]
)
def test_order_lifecycle_from_creation_to_fulfillment(self, sample_order):
"""๐ Complete Order Lifecycle Documentation
This test documents the complete order flow:
1. Order Creation โจ
2. Payment Processing ๐ณ
3. Inventory Check ๐ฆ
4. Shipping ๐
5. Completion ๐
"""
# Step 1: Create order
assert sample_order.status == "pending"
assert sample_order.total == Decimal("35.97")
# Step 2: Process payment
payment_result = sample_order.process_payment(
card_number="4242-4242-4242-4242",
cvv="123"
)
assert payment_result.success is True
assert sample_order.status == "paid"
# Step 3: Check inventory
inventory_check = sample_order.verify_inventory()
assert inventory_check.all_items_available is True
assert sample_order.status == "confirmed"
# Step 4: Ship order
tracking_number = sample_order.ship_order()
assert tracking_number.startswith("TRK")
assert sample_order.status == "shipped"
# Step 5: Mark delivered
sample_order.mark_delivered()
assert sample_order.status == "completed"
assert sample_order.completed_at is not None
def test_order_cancellation_before_shipping(self):
"""๐ซ Order Cancellation Rules
Documents when orders can be cancelled:
- โ
Before payment: Always allowed
- โ
After payment: Allowed with refund
- โ After shipping: Not allowed
"""
order = Order(customer_email="[email protected]")
# Can cancel unpaid orders
assert order.can_cancel() is True
order.cancel()
assert order.status == "cancelled"
# Can cancel paid orders (with refund)
paid_order = Order(customer_email="[email protected]")
paid_order.process_payment("4242-4242-4242-4242", "123")
assert paid_order.can_cancel() is True
refund = paid_order.cancel()
assert refund.amount == paid_order.total
# Cannot cancel shipped orders
shipped_order = Order(customer_email="[email protected]")
shipped_order.status = "shipped"
assert shipped_order.can_cancel() is False
๐ฎ Example 2: Game Scoring System with BDD
Using behave for behavior-driven documentation:
# ๐ฎ features/game_scoring.feature
Feature: Game Scoring System
As a game developer
I want to document our scoring system
So players understand how points work
Background:
Given a new game session ๐ฎ
Scenario: Basic scoring for collecting coins
"""
๐ช Coin Collection Scoring:
- Regular coins: 10 points
- Silver coins: 25 points
- Gold coins: 100 points
"""
When the player collects 5 regular coins
And the player collects 2 silver coins
And the player collects 1 gold coin
Then the score should be 200 points
And the coin combo multiplier should be active
Scenario: Combo multiplier increases points
"""
โก Combo System:
- 5 coins in 10 seconds: 2x multiplier
- 10 coins in 10 seconds: 3x multiplier
- 15+ coins in 10 seconds: 5x multiplier
"""
When the player rapidly collects 10 coins
Then the combo multiplier should be 3x
And subsequent coins should give triple points
Scenario Outline: Level completion bonuses
"""
๐ Level Completion Bonuses:
- Time bonus: 1000 - (seconds * 10)
- Perfect run: +500 points
- No damage: +300 points
"""
Given the player completes level <level>
When completion time is <time> seconds
And the player took <damage> damage
Then the time bonus should be <time_bonus> points
And the total bonus should be <total_bonus> points
Examples:
| level | time | damage | time_bonus | total_bonus |
| 1 | 30 | 0 | 700 | 1500 |
| 1 | 60 | 1 | 400 | 400 |
| 2 | 45 | 0 | 550 | 1350 |
# ๐ฏ steps/game_scoring_steps.py
from behave import given, when, then
@given('a new game session ๐ฎ')
def step_new_game(context):
"""Initialize a fresh game session"""
context.game = GameSession()
context.game.start()
@when('the player collects {count:d} regular coins')
def step_collect_coins(context, count):
"""Document coin collection mechanics"""
for _ in range(count):
context.game.collect_coin("regular")
@then('the score should be {expected:d} points')
def step_verify_score(context, expected):
"""Verify scoring calculations"""
assert context.game.score == expected, \
f"Expected {expected} points but got {context.game.score}"
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Generating Documentation from Tests
When youโre ready to level up, automatically generate docs:
# ๐ฏ doc_generator.py
import inspect
import pytest
from pathlib import Path
class DocumentationGenerator:
"""โจ Generate beautiful docs from test suites"""
def generate_from_tests(self, test_module):
"""๐ช Transform tests into readable documentation
Examples:
>>> gen = DocumentationGenerator()
>>> docs = gen.generate_from_tests(test_shopping_cart)
>>> print(docs)
# Shopping Cart API
## Adding Items
- Adding items to cart increases total
- Multiple items accumulate correctly
## Discounts
- Discount codes reduce total
- Invalid codes show error message
"""
docs = []
# ๐ Extract class docstring
if hasattr(test_module, '__doc__'):
docs.append(f"# {test_module.__doc__}")
# ๐ Find all test classes
for name, obj in inspect.getmembers(test_module):
if inspect.isclass(obj) and name.startswith('Test'):
docs.append(f"\n## {obj.__name__[4:]}") # Remove 'Test' prefix
# ๐ Extract test methods
for method_name, method in inspect.getmembers(obj):
if method_name.startswith('test_'):
# Convert test name to readable format
readable_name = method_name[5:].replace('_', ' ').title()
docs.append(f"- {readable_name}")
# Add docstring if available
if method.__doc__:
docs.append(f" {method.__doc__.strip()}")
return '\n'.join(docs)
# ๐ Usage with pytest hooks
def pytest_collection_modifyitems(config, items):
"""Hook to generate docs during test collection"""
doc_gen = DocumentationGenerator()
docs = doc_gen.generate_from_tests(items)
# ๐พ Save to markdown file
Path("API_DOCS.md").write_text(docs)
๐๏ธ Advanced Topic 2: Interactive Documentation
For the brave developers, create interactive docs:
# ๐ interactive_docs.py
import pytest
from IPython.display import Markdown, display
import ast
class InteractiveDocumentation:
"""๐จ Create Jupyter-friendly test documentation"""
def run_and_document(self, test_func):
"""๐ Execute test and display results as documentation
This creates beautiful, interactive documentation
in Jupyter notebooks!
"""
# ๐ Extract test metadata
test_name = test_func.__name__
docstring = test_func.__doc__ or "No description"
# ๐ฏ Parse test code for examples
source = inspect.getsource(test_func)
examples = self._extract_examples(source)
# ๐ Run the test
try:
test_func()
status = "โ
PASSED"
status_color = "green"
except Exception as e:
status = f"โ FAILED: {str(e)}"
status_color = "red"
# ๐จ Create beautiful output
markdown = f"""
## {test_name.replace('test_', '').replace('_', ' ').title()}
**Status:** <span style="color: {status_color}">{status}</span>
### Description
{docstring}
### Code Example
```python
{examples}
Try It Yourself! ๐ฎ
Run the cell below to test this functionality: """
display(Markdown(markdown))
๐ฏ Create executable cell
return self._create_executable_cell(test_func)
def _extract_examples(self, source): """๐ Extract meaningful code examples from test"""
Parse and find assert statements
tree = ast.parse(source) examples = []
for node in ast.walk(tree): if isinstance(node, ast.Assert): examples.append(ast.unparse(node.test))
return โ\nโ.join(examples[:3]) # First 3 examples
## โ ๏ธ Common Pitfalls and Solutions
### ๐ฑ Pitfall 1: Tests That Don't Document
```python
# โ Wrong way - unclear test names!
def test_1():
"""Test case 1""" # ๐ฐ What does this test?
obj = Thing()
assert obj.do_stuff() == 42
# โ
Correct way - self-documenting!
def test_calculate_fibonacci_returns_correct_sequence():
"""๐ข Fibonacci calculation should return the correct sequence
The Fibonacci sequence starts with 0, 1 and each subsequent
number is the sum of the previous two numbers.
Example: 0, 1, 1, 2, 3, 5, 8, 13...
"""
fib = FibonacciCalculator()
assert fib.get_sequence(5) == [0, 1, 1, 2, 3]
assert fib.get_nth(7) == 8 # 7th number is 8
๐คฏ Pitfall 2: Documentation That Doesnโt Run
# โ Dangerous - documentation might be wrong!
"""
API Usage:
cart = Cart()
cart.add("item") # This might not work anymore!
cart.total() # Method might be renamed!
"""
# โ
Safe - documentation that verifies itself!
def test_api_usage_documentation():
"""๐ API Usage Examples (Verified)
These examples are tested with every build,
ensuring they always work!
"""
# Create a shopping cart
cart = ShoppingCart()
# Add items (with current API)
cart.add_item("Python Book", price=29.99)
cart.add_item("Coffee Mug", price=12.99)
# Get total (verified method name)
assert cart.get_total() == 42.98
# This IS the documentation! ๐
๐ ๏ธ Best Practices
- ๐ฏ Descriptive Test Names: Use
test_what_when_then
pattern - ๐ Rich Docstrings: Include context, examples, and expected behavior
- ๐ก๏ธ Test as Examples: Write tests that serve as usage examples
- ๐จ Group Related Tests: Use test classes for logical grouping
- โจ Keep It Readable: Prioritize clarity over cleverness
๐งช Hands-On Exercise
๐ฏ Challenge: Create Living Documentation for a Library System
Build living documentation for a library management system:
๐ Requirements:
- โ Book checkout/return documentation
- ๐ท๏ธ Late fee calculation examples
- ๐ค Member registration process
- ๐ Reservation system behavior
- ๐จ Search functionality examples
๐ Bonus Points:
- Generate HTML documentation from tests
- Add interactive examples
- Create visual test reports
๐ก Solution
๐ Click to see solution
# ๐ฏ test_library_system.py - Living Documentation
import pytest
from datetime import datetime, timedelta
from decimal import Decimal
class TestLibrarySystem:
"""๐ Library Management System - Living Documentation
This test suite serves as the official documentation
for our library management system API.
"""
# ๐ Book Management Documentation
def test_checking_out_books_creates_loan_record(self):
"""๐ Book Checkout Process
When a member checks out a book:
1. Book status changes to 'checked_out'
2. Loan record is created with due date
3. Member's active loans increase
Example:
member = Member("[email protected]")
book = Book("Python Crash Course", isbn="978-1593279288")
loan = library.checkout(member, book)
# Loan is active for 14 days by default
assert loan.due_date == today + timedelta(days=14)
"""
library = Library()
member = library.register_member("[email protected]", "Alice Smith")
book = library.add_book("Python Crash Course", "978-1593279288")
# Checkout process
loan = library.checkout_book(member.id, book.isbn)
# Verify loan details
assert loan.member_id == member.id
assert loan.book_isbn == book.isbn
assert loan.due_date == datetime.now().date() + timedelta(days=14)
assert book.status == "checked_out"
assert len(member.active_loans) == 1
def test_late_fee_calculation_with_examples(self):
"""๐ฐ Late Fee Calculation Rules
Late fees are calculated as follows:
- First 7 days: $0.50 per day
- Days 8-30: $1.00 per day
- After 30 days: $2.00 per day + $25 processing fee
- Maximum fee: $50.00
Examples shown in test cases below!
"""
library = Library()
# Example 1: 3 days late = $1.50
fee_3_days = library.calculate_late_fee(days_late=3)
assert fee_3_days == Decimal("1.50")
# Example 2: 10 days late = $3.50 + $3.00 = $6.50
fee_10_days = library.calculate_late_fee(days_late=10)
assert fee_10_days == Decimal("6.50")
# Example 3: 35 days late = $3.50 + $23.00 + $10.00 + $25.00 = $50.00 (max)
fee_35_days = library.calculate_late_fee(days_late=35)
assert fee_35_days == Decimal("50.00") # Capped at maximum
# ๐ Search Functionality Documentation
def test_book_search_supports_multiple_criteria(self):
"""๐ Book Search API Examples
The search API supports:
- Title search (partial match)
- Author search (case-insensitive)
- ISBN lookup (exact match)
- Genre filtering
- Availability filtering
Complex queries can combine multiple criteria.
"""
library = Library()
# Add test books
library.add_book("Clean Code", "978-0132350884", "Robert Martin", "Programming")
library.add_book("Clean Architecture", "978-0134494166", "Robert Martin", "Programming")
library.add_book("The Clean Coder", "978-0137081073", "Robert Martin", "Programming")
# Example 1: Search by partial title
results = library.search(title="Clean")
assert len(results) == 3
# Example 2: Search by author
results = library.search(author="robert martin") # Case insensitive
assert len(results) == 3
# Example 3: Combined search
results = library.search(
title="Code",
genre="Programming",
available_only=True
)
assert len(results) == 2 # "Clean Code" and "The Clean Coder"
# Example 4: ISBN lookup (exact)
book = library.search(isbn="978-0132350884")
assert book.title == "Clean Code"
# ๐ฅ Member Management Documentation
@pytest.mark.parametrize("member_type,book_limit,loan_period", [
("student", 3, 14), # Students: 3 books, 2 weeks
("faculty", 10, 30), # Faculty: 10 books, 1 month
("community", 5, 21), # Community: 5 books, 3 weeks
])
def test_member_privileges_by_type(self, member_type, book_limit, loan_period):
"""๐ฅ Member Types and Privileges
Different member types have different privileges:
| Member Type | Book Limit | Loan Period |
|-------------|------------|-------------|
| Student | 3 books | 14 days |
| Faculty | 10 books | 30 days |
| Community | 5 books | 21 days |
"""
library = Library()
member = library.register_member(
email=f"{member_type}@example.com",
name=f"Test {member_type.title()}",
member_type=member_type
)
assert member.book_limit == book_limit
assert member.loan_period_days == loan_period
# ๐
Reservation System Documentation
def test_reservation_queue_processing(self):
"""๐
Book Reservation System
How reservations work:
1. Members can reserve checked-out books
2. Reservations are processed in FIFO order
3. Members are notified when book is available
4. Reserved books are held for 3 days
This test demonstrates the complete flow!
"""
library = Library()
book = library.add_book("Popular Python Book", "978-1234567890")
# Alice checks out the book
alice = library.register_member("[email protected]", "Alice")
library.checkout_book(alice.id, book.isbn)
# Bob and Carol want to reserve it
bob = library.register_member("[email protected]", "Bob")
carol = library.register_member("[email protected]", "Carol")
bob_reservation = library.reserve_book(bob.id, book.isbn)
carol_reservation = library.reserve_book(carol.id, book.isbn)
assert bob_reservation.queue_position == 1
assert carol_reservation.queue_position == 2
# Alice returns the book
library.return_book(alice.id, book.isbn)
# Bob gets notified (first in queue)
assert bob_reservation.status == "ready_for_pickup"
assert book.status == "reserved"
assert book.reserved_for == bob.id
assert book.hold_expires == datetime.now().date() + timedelta(days=3)
# Carol is now first in queue
carol_reservation.refresh()
assert carol_reservation.queue_position == 1
# ๐ฏ Generate documentation from tests
def generate_library_docs():
"""๐ Generate HTML documentation from test suite"""
import pytest
import json
# Run tests and collect results
pytest.main([
'test_library_system.py',
'--tb=no',
'--json-report',
'--json-report-file=test_results.json'
])
# Parse results and generate docs
with open('test_results.json') as f:
results = json.load(f)
# Create beautiful HTML documentation
html_template = """
<html>
<head>
<title>๐ Library System API Documentation</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.test { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
.passed { border-left: 5px solid #28a745; }
.failed { border-left: 5px solid #dc3545; }
pre { background: #f4f4f4; padding: 10px; overflow-x: auto; }
.emoji { font-size: 1.2em; }
</style>
</head>
<body>
<h1>๐ Library System API Documentation</h1>
<p>Generated from test suite on {date}</p>
{test_docs}
</body>
</html>
"""
# Build test documentation sections
test_docs = []
for test in results['tests']:
status_class = 'passed' if test['outcome'] == 'passed' else 'failed'
test_docs.append(f"""
<div class="test {status_class}">
<h3>{test['nodeid'].split('::')[-1]}</h3>
<p>{test.get('call', {}).get('docstring', 'No description')}</p>
</div>
""")
# Save documentation
with open('library_api_docs.html', 'w') as f:
f.write(html_template.format(
date=datetime.now().strftime('%Y-%m-%d %H:%M'),
test_docs='\n'.join(test_docs)
))
print("๐ Documentation generated: library_api_docs.html")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create living documentation that stays current ๐ช
- โ Write self-documenting tests that serve as examples ๐ก๏ธ
- โ Use doctest for inline documentation ๐ฏ
- โ Generate documentation from test suites ๐
- โ Build interactive documentation with Python! ๐
Remember: Your tests are not just quality assurance - theyโre your best documentation! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered living documentation in Python testing!
Hereโs what to do next:
- ๐ป Convert your existing tests to living documentation
- ๐๏ธ Set up automatic documentation generation in CI/CD
- ๐ Explore tools like Sphinx for test documentation
- ๐ Share your self-documenting tests with your team!
Remember: The best documentation is the one that never lies - because itโs tested with every commit! Keep documenting, keep testing, and most importantly, have fun! ๐
Happy testing and documenting! ๐๐โจ