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