TDD in Python: Writing Clean and Testable Code
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