doctest
doctest
Overview
doctest is a documentation testing module included in Python's standard library. It searches for pieces of text that look like interactive Python sessions, and then executes those sessions to verify that they work exactly as shown. As a lightweight testing framework, it provides quick and straightforward test automation from your project's documentation.
Details
Key Features
Documentation-Integrated Testing
- Reads and executes test cases from docstrings and documentation
- Automatically ensures synchronization between documentation and tests
- Test cases written in interactive Python session format
Standard Library Built-in
- Included with Python by default, no additional installation required
- Uses
>>>prompt (Primary) and...prompt (Secondary) - Expected output results written immediately below
Automatic Test Discovery
- Searches module docstrings, functions, classes, and method docstrings
- Objects imported into the module are not searched
- Automatic test execution and result verification
CI/CD Pipeline Integration
- Can be added to modern development workflows as of 2024
- Enables continuous maintenance of documentation quality
Pros and Cons
Pros
- Documentation Synchronization: Automatically ensures consistency between documentation and code
- Low Learning Curve: Easy to use as a Python standard feature
- Lightweight: No additional dependencies or setup required
- Practical Examples: Can include actually working code examples in documentation
- Immediate Feedback: Verify accuracy of documentation examples with a single command
Cons
- Limited Test Coverage: Mainly applicable to small code snippets or simple functions
- Output Comparison Limitations: Only compares printed output; variable output causes test failures
- Unsuitable for Complex Tests: Cannot cover all aspects of complex functions or classes
- Limited Error Messages: Standard error messages can sometimes be insufficient
Reference Links
- Official Documentation: doctest — Python Documentation
- Real Python: Python doctest: Document and Test Your Code at Once
- Python Module of the Week: doctest – Testing through documentation
- GeeksforGeeks: Testing in Python using Doctest module
Code Examples
Basic doctest Examples
def add(a, b):
"""
Add two numbers.
>>> add(2, 3)
5
>>> add(-1, 1)
0
>>> add(0, 0)
0
"""
return a + b
def multiply(a, b):
"""
Multiply two numbers.
>>> multiply(3, 4)
12
>>> multiply(-2, 5)
-10
>>> multiply(0, 100)
0
"""
return a * b
if __name__ == "__main__":
import doctest
doctest.testmod()
String Operations doctest
def reverse_string(text):
"""
Reverse a string.
>>> reverse_string("hello")
'olleh'
>>> reverse_string("Python")
'nohtyP'
>>> reverse_string("")
''
>>> reverse_string("a")
'a'
"""
return text[::-1]
def count_vowels(text):
"""
Count vowels in a string.
>>> count_vowels("hello")
2
>>> count_vowels("Python")
1
>>> count_vowels("aeiou")
5
>>> count_vowels("xyz")
0
>>> count_vowels("")
0
"""
vowels = "aeiouAEIOU"
return sum(1 for char in text if char in vowels)
List and Collection Operations
def find_max(numbers):
"""
Find the maximum value in a list.
>>> find_max([1, 2, 3, 4, 5])
5
>>> find_max([-1, -2, -3])
-1
>>> find_max([42])
42
>>> find_max([])
Traceback (most recent call last):
...
ValueError: List is empty
"""
if not numbers:
raise ValueError("List is empty")
return max(numbers)
def remove_duplicates(items):
"""
Remove duplicates from a list while preserving order.
>>> remove_duplicates([1, 2, 2, 3, 1, 4])
[1, 2, 3, 4]
>>> remove_duplicates(['a', 'b', 'a', 'c'])
['a', 'b', 'c']
>>> remove_duplicates([])
[]
>>> remove_duplicates([1, 1, 1])
[1]
"""
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
Class doctest
class Calculator:
"""
A simple calculator class.
>>> calc = Calculator()
>>> calc.add(5, 3)
8
>>> calc.subtract(10, 4)
6
>>> calc.multiply(6, 7)
42
>>> calc.divide(15, 3)
5.0
>>> calc.divide(10, 0)
Traceback (most recent call last):
...
ZeroDivisionError: Cannot divide by zero
"""
def add(self, a, b):
"""
>>> calc = Calculator()
>>> calc.add(2, 3)
5
"""
return a + b
def subtract(self, a, b):
"""
>>> calc = Calculator()
>>> calc.subtract(10, 4)
6
"""
return a - b
def multiply(self, a, b):
"""
>>> calc = Calculator()
>>> calc.multiply(3, 4)
12
"""
return a * b
def divide(self, a, b):
"""
>>> calc = Calculator()
>>> calc.divide(15, 3)
5.0
>>> calc.divide(1, 0)
Traceback (most recent call last):
...
ZeroDivisionError: Cannot divide by zero
"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
Exception Handling Tests
def validate_age(age):
"""
Validate age value.
>>> validate_age(25)
True
>>> validate_age(0)
True
>>> validate_age(120)
True
>>> validate_age(-1)
Traceback (most recent call last):
...
ValueError: Age cannot be negative
>>> validate_age(150)
Traceback (most recent call last):
...
ValueError: Age cannot exceed 150
>>> validate_age("25")
Traceback (most recent call last):
...
TypeError: Age must be an integer
"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age cannot exceed 150")
return True
def parse_email(email):
"""
Parse an email address.
>>> parse_email("[email protected]")
('user', 'example.com')
>>> parse_email("[email protected]")
('test.email', 'domain.org')
>>> parse_email("invalid-email")
Traceback (most recent call last):
...
ValueError: Invalid email format
>>> parse_email("")
Traceback (most recent call last):
...
ValueError: Invalid email format
"""
if "@" not in email:
raise ValueError("Invalid email format")
parts = email.split("@")
if len(parts) != 2 or not parts[0] or not parts[1]:
raise ValueError("Invalid email format")
return parts[0], parts[1]
File Test Execution
# math_utils.py
def factorial(n):
"""
Calculate factorial.
>>> factorial(5)
120
>>> factorial(0)
1
>>> factorial(1)
1
"""
if n < 0:
raise ValueError("Factorial is not defined for negative numbers")
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
# Execution method
if __name__ == "__main__":
import doctest
# Run tests in verbose mode
doctest.testmod(verbose=True)
External File doctest
# test_examples.txt file
Mathematical Calculation Tests
===============================
Addition examples:
>>> 2 + 3
5
>>> 10 + (-5)
5
String manipulation examples:
>>> "Hello" + " " + "World"
'Hello World'
>>> "Python".upper()
'PYTHON'
List operation examples:
>>> numbers = [1, 2, 3, 4, 5]
>>> len(numbers)
5
>>> numbers[0]
1
>>> numbers[-1]
5
# Execution code
if __name__ == "__main__":
import doctest
# Run external file tests
doctest.testfile("test_examples.txt", verbose=True)
Custom Configuration doctest Execution
def division_with_precision(a, b, precision=2):
"""
Perform division with specified precision.
>>> division_with_precision(22, 7) # doctest: +ELLIPSIS
3.1...
>>> division_with_precision(1, 3, 4)
0.3333
>>> division_with_precision(10, 3) # doctest: +SKIP
3.33
"""
return round(a / b, precision)
def get_current_time():
"""
Get current time (ignored in tests).
>>> get_current_time() # doctest: +SKIP
'2024-01-01 12:00:00'
"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test execution with flags
if __name__ == "__main__":
import doctest
# Allow ellipsis notation and normalized whitespace
doctest.testmod(
optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
)
pytest Integration with doctest
# Configuration in pytest.ini or pyproject.toml
[tool.pytest.ini_options]
addopts = "--doctest-modules"
# Or at runtime
# pytest --doctest-modules
def fibonacci(n):
"""
Return the nth value in the Fibonacci sequence.
>>> fibonacci(0)
0
>>> fibonacci(1)
1
>>> fibonacci(5)
5
>>> fibonacci(10)
55
"""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# pytest doctest execution example
def test_fibonacci_doctests():
"""Run doctest with pytest"""
import doctest
import sys
current_module = sys.modules[__name__]
result = doctest.testmod(current_module, verbose=True)
assert result.failed == 0, f"{result.failed} doctests failed"
Practical doctest Design Patterns
class BankAccount:
"""
Bank account class.
Basic usage:
>>> account = BankAccount("Alice", 1000)
>>> account.get_balance()
1000
>>> account.deposit(500)
>>> account.get_balance()
1500
>>> account.withdraw(200)
>>> account.get_balance()
1300
Error cases:
>>> account.withdraw(2000)
Traceback (most recent call last):
...
ValueError: Insufficient balance
>>> account.withdraw(-100)
Traceback (most recent call last):
...
ValueError: Amount must be positive
"""
def __init__(self, name, initial_balance=0):
"""
Create a new account.
>>> account = BankAccount("Bob")
>>> account.get_balance()
0
>>> account.name
'Bob'
"""
self.name = name
self.balance = initial_balance
def deposit(self, amount):
"""
Deposit the specified amount.
>>> account = BankAccount("Charlie", 100)
>>> account.deposit(50)
>>> account.get_balance()
150
"""
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
def withdraw(self, amount):
"""
Withdraw the specified amount.
>>> account = BankAccount("David", 200)
>>> account.withdraw(50)
>>> account.get_balance()
150
"""
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient balance")
self.balance -= amount
def get_balance(self):
"""
Get the current balance.
>>> account = BankAccount("Eve", 300)
>>> account.get_balance()
300
"""
return self.balance
# Test execution and coverage verification
if __name__ == "__main__":
import doctest
result = doctest.testmod(verbose=True)
print(f"\nTest results: {result.attempted} attempted, {result.failed} failed")