Skip to main content
โšก Calmops

Testing Strategies: Unit, Integration, and E2E Testing for Modern Applications

Introduction

Testing is fundamental to software quality. A well-tested codebase enables confident refactoring, catches bugs early, and serves as documentation. This guide covers testing strategies, test types, and best practices.

Test Pyramid

The test pyramid guides test distribution: many fast unit tests at the base, fewer integration tests in the middle, and few slow E2E tests at the top.

# tests/test_user_service.py
import pytest
from unittest.mock import Mock, patch
from datetime import datetime

class TestUserService:
    """Unit tests for user service."""
    
    @pytest.fixture
    def user_repo(self):
        return Mock()
    
    @pytest.fixture
    def email_service(self):
        return Mock()
    
    @pytest.fixture
    def service(self, user_repo, email_service):
        from services.user_service import UserService
        return UserService(user_repo, email_service)
    
    def test_create_user_success(self, service, user_repo, email_service):
        """Test successful user creation."""
        user_repo.find_by_email.return_value = None
        
        result = service.create_user(
            email="[email protected]",
            name="Test User",
            password="secure123"
        )
        
        assert result.email == "[email protected]"
        assert result.name == "Test User"
        assert result.id is not None
        user_repo.save.assert_called_once()
        email_service.send_welcome.assert_called_once()
    
    def test_create_user_duplicate_email(self, service, user_repo):
        """Test user creation with existing email."""
        user_repo.find_by_email.return_value = Mock(id="existing")
        
        with pytest.raises(ValueError, match="already exists"):
            service.create_user(
                email="[email protected]",
                name="Test",
                password="password123"
            )
    
    def test_create_user_invalid_email(self, service):
        """Test user creation with invalid email."""
        with pytest.raises(ValueError, match="valid email"):
            service.create_user(
                email="invalid",
                name="Test",
                password="password123"
            )

# tests/test_api.py
from fastapi.testclient import TestClient

class TestUserAPI:
    """Integration tests for user API."""
    
    @pytest.fixture
    def client(self, test_db):
        from main import app
        from dependencies import get_db
        from database import get_engine
        
        TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=get_engine())
        
        def override_get_db():
            db = TestingSessionLocal()
            try:
                yield db
            finally:
                db.close()
        
        app.dependency_overrides[get_db] = override_get_db
        
        with TestClient(app) as client:
            yield client
        
        app.dependency_overrides.clear()
    
    def test_create_and_get_user(self, client):
        """Test creating and retrieving a user."""
        # Create user
        response = client.post(
            "/api/v1/users",
            json={
                "email": "[email protected]",
                "name": "API Test",
                "password": "password123"
            }
        )
        assert response.status_code == 201
        user_id = response.json()["id"]
        
        # Get user
        response = client.get(f"/api/v1/users/{user_id}")
        assert response.status_code == 200
        assert response.json()["email"] == "[email protected]"

Mocking and Fixtures

# tests/conftest.py
import pytest
from unittest.mock import Mock, MagicMock
from datetime import datetime, timedelta

@pytest.fixture
def mock_user():
    """Create a mock user object."""
    user = Mock()
    user.id = "test-id"
    user.email = "[email protected]"
    user.name = "Test User"
    user.is_active = True
    user.created_at = datetime.utcnow()
    return user

@pytest.fixture
def mock_db_session():
    """Create a mock database session."""
    session = MagicMock()
    session.query.return_value.filter.return_value.first.return_value = None
    return session

@pytest.fixture
def sample_users():
    """Create sample users for testing."""
    return [
        {"id": "1", "email": "[email protected]", "name": "User 1"},
        {"id": "2", "email": "[email protected]", "name": "User 2"},
        {"id": "3", "email": "[email protected]", "name": "User 3"},
    ]

class TestMockingPatterns:
    """Examples of common mocking patterns."""
    
    def test_method_call_verification(self, mock_user):
        """Verify a method was called."""
        service = Mock()
        service.create_user.return_value = mock_user
        
        result = service.create_user("[email protected]", "Test")
        
        service.create_user.assert_called_once_with("[email protected]", "Test")
        assert result.email == "[email protected]"
    
    def test_return_value_side_effect(self, mock_user):
        """Use side effect for complex behavior."""
        service = Mock()
        service.get_user.side_effect = [
            None,  # First call returns None
            mock_user,  # Second call returns user
        ]
        
        assert service.get_user("1") is None
        assert service.get_user("2").email == "[email protected]"
    
    def test_property_mock(self):
        """Mock properties and attributes."""
        mock = Mock()
        mock.user.name = "Test"
        mock.user.email = "[email protected]"
        
        assert mock.user.name == "Test"

Conclusion

Effective testing requires strategy: write many fast unit tests, some integration tests, and few E2E tests. Mock external dependencies. Use fixtures for setup. Test behavior, not implementation. Aim for meaningful coverage, not arbitrary numbers.

Resources

  • pytest Documentation
  • “Working Effectively with Legacy Code” by Michael Feathers
  • Test-Driven Development by Kent Beck

Comments