Introduction
Python's simplicity and readability make it an excellent language for Test-Driven Development. This article explores TDD practices in Python using pytest and other modern testing tools.
Setting Up a TDD Environment
# requirements.txt
pytest==7.0.0
pytest-cov==4.0.0
pytest-mock==3.10.0
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --cov=src --cov-report=term-missing
Writing Tests with pytest
# test_calculator.py
import pytest
from src.calculator import Calculator
def test_add_positive_numbers():
calculator = Calculator()
assert calculator.add(2, 3) == 5
def test_add_negative_numbers():
calculator = Calculator()
assert calculator.add(-2, -3) == -5
def test_add_mixed_numbers():
calculator = Calculator()
assert calculator.add(-2, 3) == 1
# calculator.py
class Calculator:
def add(self, a, b):
return a + b
Using Fixtures
# test_user.py
import pytest
from src.user import User
@pytest.fixture
def valid_user_data():
return {
'name': 'John Doe',
'email': 'john@example.com'
}
@pytest.fixture
def user_service():
return UserService()
def test_create_user(user_service, valid_user_data):
user = user_service.create_user(valid_user_data)
assert user.name == valid_user_data['name']
assert user.email == valid_user_data['email']
def test_validate_email(user_service):
with pytest.raises(ValueError) as exc_info:
user_service.create_user({
'name': 'John',
'email': 'invalid-email'
})
assert 'Invalid email format' in str(exc_info.value)
Mocking Dependencies
# test_order_service.py
import pytest
from unittest.mock import Mock
def test_process_order(mocker):
# Arrange
mock_payment_gateway = Mock()
mock_payment_gateway.process_payment.return_value = {'success': True}
mock_email_service = Mock()
order_service = OrderService(
mock_payment_gateway,
mock_email_service
)
order = {'id': 1, 'amount': 100}
# Act
result = order_service.process_order(order)
# Assert
assert result['success'] is True
mock_payment_gateway.process_payment.assert_called_once_with(order)
mock_email_service.send_confirmation.assert_called_once_with(order)
Testing Async Code
# test_async_service.py
import pytest
import asyncio
@pytest.mark.asyncio
async def test_fetch_user():
service = UserService()
user = await service.fetch_user(1)
assert user['id'] == 1
assert 'name' in user
@pytest.mark.asyncio
async def test_fetch_user_error():
service = UserService()
with pytest.raises(UserNotFoundError):
await service.fetch_user(999)
Best Practices
- Use descriptive test names
- Follow the Arrange-Act-Assert pattern
- Use fixtures for common setup
- Mock external dependencies
- Test edge cases and error conditions
Conclusion
Python's testing ecosystem, combined with its readability and simplicity, makes it an excellent choice for Test-Driven Development. By following these practices, you can create robust and maintainable Python applications.
"Python's testing tools make it easy to write clean, maintainable tests that drive the development process." - Guido van Rossum
Member discussion: