Chapter 12 - Real-World Projects and Best Practices

Haiyue
22min

Chapter 12: Real-World Projects and Best Practices

Learning Objectives
  • Design complete test project structure
  • Implement end-to-end testing solutions
  • Master Test-Driven Development (TDD) practices
  • Summarize pytest best practices

Knowledge Points

Project Testing Architecture

A complete test project should include:

  • Clear directory structure: Separate different types of tests
  • Configuration management: Unified configuration files and environment management
  • Tool integration: Integration with CI/CD, code coverage, static analysis tools
  • Documentation and standards: Testing guidelines and specifications

Testing Pyramid

🔄 正在渲染 Mermaid 图表...
  • Unit tests (70%): Fast, independent, numerous
  • Integration tests (20%): Medium speed, test component interactions
  • End-to-end tests (10%): Slow, test complete workflows

Example Code

Complete Project Structure

project/
├── src/
│   ├── __init__.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── product.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── auth_service.py
│   │   └── product_service.py
│   ├── utils/
│   │   ├── __init__.py
│   │   └── helpers.py
│   └── main.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── unit/
│   │   ├── __init__.py
│   │   ├── test_models/
│   │   ├── test_services/
│   │   └── test_utils/
│   ├── integration/
│   │   ├── __init__.py
│   │   └── test_api/
│   ├── e2e/
│   │   ├── __init__.py
│   │   └── test_workflows/
│   └── fixtures/
│       ├── __init__.py
│       └── data/
├── pytest.ini
├── requirements.txt
├── requirements-test.txt
└── README.md

Real-World Project: User Management System

# src/models/user.py
from dataclasses import dataclass
from typing import Optional
import hashlib
import re

@dataclass
class User:
    """User model"""
    username: str
    email: str
    password_hash: str
    id: Optional[int] = None
    is_active: bool = True

    def __post_init__(self):
        self.validate()

    def validate(self):
        """Validate user data"""
        if not self.username or len(self.username) < 3:
            raise ValueError("Username must be at least 3 characters")

        if not re.match(r'^[a-zA-Z0-9_]+$', self.username):
            raise ValueError("Username can only contain letters, numbers and underscores")

        if not self.email or '@' not in self.email:
            raise ValueError("Invalid email address")

    @classmethod
    def create(cls, username: str, email: str, password: str):
        """Create new user"""
        if len(password) < 6:
            raise ValueError("Password must be at least 6 characters")

        password_hash = hashlib.sha256(password.encode()).hexdigest()
        return cls(username=username, email=email, password_hash=password_hash)

    def check_password(self, password: str) -> bool:
        """Verify password"""
        return self.password_hash == hashlib.sha256(password.encode()).hexdigest()
# src/services/user_service.py
from typing import List, Optional
from ..models.user import User

class UserRepository:
    """User repository (simulated)"""
    def __init__(self):
        self._users = {}
        self._next_id = 1

    def save(self, user: User) -> User:
        """Save user"""
        if user.id is None:
            user.id = self._next_id
            self._next_id += 1
        self._users[user.id] = user
        return user

    def find_by_id(self, user_id: int) -> Optional[User]:
        """Find user by ID"""
        return self._users.get(user_id)

    def find_by_username(self, username: str) -> Optional[User]:
        """Find user by username"""
        for user in self._users.values():
            if user.username == username:
                return user
        return None

    def find_all(self) -> List[User]:
        """Find all users"""
        return list(self._users.values())

    def delete(self, user_id: int) -> bool:
        """Delete user"""
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False

class UserService:
    """User service"""
    def __init__(self, repository: UserRepository):
        self.repository = repository

    def register_user(self, username: str, email: str, password: str) -> User:
        """Register new user"""
        # Check if username already exists
        existing_user = self.repository.find_by_username(username)
        if existing_user:
            raise ValueError(f"Username '{username}' already exists")

        # Create and save user
        user = User.create(username, email, password)
        return self.repository.save(user)

    def authenticate(self, username: str, password: str) -> Optional[User]:
        """User authentication"""
        user = self.repository.find_by_username(username)
        if user and user.is_active and user.check_password(password):
            return user
        return None

    def get_user(self, user_id: int) -> Optional[User]:
        """Get user"""
        return self.repository.find_by_id(user_id)

    def update_user(self, user_id: int, **kwargs) -> Optional[User]:
        """Update user information"""
        user = self.repository.find_by_id(user_id)
        if not user:
            return None

        # Update allowed fields
        if 'email' in kwargs:
            user.email = kwargs['email']
            user.validate()  # Re-validate

        return self.repository.save(user)

    def deactivate_user(self, user_id: int) -> bool:
        """Deactivate user"""
        user = self.repository.find_by_id(user_id)
        if user:
            user.is_active = False
            self.repository.save(user)
            return True
        return False

Complete Test Suite

# tests/conftest.py
import pytest
from src.models.user import User
from src.services.user_service import UserRepository, UserService

@pytest.fixture
def user_repository():
    """User repository fixture"""
    return UserRepository()

@pytest.fixture
def user_service(user_repository):
    """User service fixture"""
    return UserService(user_repository)

@pytest.fixture
def sample_user():
    """Sample user fixture"""
    return User.create("testuser", "test@example.com", "password123")

@pytest.fixture
def registered_user(user_service):
    """Registered user fixture"""
    return user_service.register_user("john_doe", "john@example.com", "securepass")

@pytest.fixture
def multiple_users(user_service):
    """Multiple users fixture"""
    users = []
    for i in range(3):
        user = user_service.register_user(
            f"user{i}",
            f"user{i}@example.com",
            f"password{i}"
        )
        users.append(user)
    return users
# tests/unit/test_models/test_user.py
import pytest
from src.models.user import User

class TestUserModel:
    """User model tests"""

    def test_user_creation(self):
        """Test user creation"""
        user = User.create("testuser", "test@example.com", "password123")

        assert user.username == "testuser"
        assert user.email == "test@example.com"
        assert user.password_hash is not None
        assert user.is_active is True
        assert user.id is None

    def test_user_validation_success(self):
        """Test user validation success"""
        # These should all succeed
        valid_users = [
            ("abc", "test@example.com", "password"),
            ("user123", "user@domain.com", "123456"),
            ("test_user", "test@test.co.uk", "longpassword")
        ]

        for username, email, password in valid_users:
            user = User.create(username, email, password)
            assert user.username == username

    @pytest.mark.parametrize("username,email,password,expected_error", [
        ("ab", "test@example.com", "password", "Username must be at least 3 characters"),
        ("", "test@example.com", "password", "Username must be at least 3 characters"),
        ("test user", "test@example.com", "password", "Username can only contain letters, numbers and underscores"),
        ("test@user", "test@example.com", "password", "Username can only contain letters, numbers and underscores"),
        ("testuser", "invalid-email", "password", "Invalid email address"),
        ("testuser", "", "password", "Invalid email address"),
        ("testuser", "test@example.com", "123", "Password must be at least 6 characters"),
        ("testuser", "test@example.com", "", "Password must be at least 6 characters"),
    ])
    def test_user_validation_failure(self, username, email, password, expected_error):
        """Test user validation failure"""
        with pytest.raises(ValueError, match=expected_error):
            User.create(username, email, password)

    def test_password_verification(self):
        """Test password verification"""
        user = User.create("testuser", "test@example.com", "password123")

        assert user.check_password("password123") is True
        assert user.check_password("wrongpassword") is False
        assert user.check_password("") is False

    def test_user_dataclass_features(self):
        """Test dataclass features"""
        user1 = User.create("testuser", "test@example.com", "password123")
        user2 = User.create("testuser", "test@example.com", "password123")

        # Test equality (same data should be equal)
        assert user1 == user2

        # Test string representation
        assert "testuser" in str(user1)
        assert "test@example.com" in str(user1)
# tests/unit/test_services/test_user_service.py
import pytest
from src.models.user import User
from src.services.user_service import UserService, UserRepository

class TestUserRepository:
    """User repository tests"""

    def test_save_and_find_user(self, user_repository, sample_user):
        """Test saving and finding user"""
        # Save user
        saved_user = user_repository.save(sample_user)
        assert saved_user.id is not None

        # Find by ID
        found_user = user_repository.find_by_id(saved_user.id)
        assert found_user == saved_user

        # Find by username
        found_by_username = user_repository.find_by_username(sample_user.username)
        assert found_by_username == saved_user

    def test_find_nonexistent_user(self, user_repository):
        """Test finding nonexistent user"""
        assert user_repository.find_by_id(999) is None
        assert user_repository.find_by_username("nonexistent") is None

    def test_delete_user(self, user_repository, sample_user):
        """Test deleting user"""
        saved_user = user_repository.save(sample_user)
        user_id = saved_user.id

        # Delete user
        assert user_repository.delete(user_id) is True
        assert user_repository.find_by_id(user_id) is None

        # Deleting again should return False
        assert user_repository.delete(user_id) is False

class TestUserService:
    """User service tests"""

    def test_register_user_success(self, user_service):
        """Test successful user registration"""
        user = user_service.register_user("newuser", "new@example.com", "password123")

        assert user.id is not None
        assert user.username == "newuser"
        assert user.email == "new@example.com"
        assert user.is_active is True

    def test_register_duplicate_username(self, user_service, registered_user):
        """Test registering duplicate username"""
        with pytest.raises(ValueError, match="Username 'john_doe' already exists"):
            user_service.register_user("john_doe", "another@example.com", "password")

    def test_authenticate_success(self, user_service, registered_user):
        """Test successful authentication"""
        user = user_service.authenticate("john_doe", "securepass")
        assert user is not None
        assert user.username == "john_doe"

    def test_authenticate_failure(self, user_service, registered_user):
        """Test authentication failure"""
        # Wrong password
        assert user_service.authenticate("john_doe", "wrongpass") is None

        # Nonexistent user
        assert user_service.authenticate("nonexistent", "anypass") is None

    def test_authenticate_inactive_user(self, user_service, registered_user):
        """Test authenticating inactive user"""
        # Deactivate user
        user_service.deactivate_user(registered_user.id)

        # Try to authenticate inactive user
        assert user_service.authenticate("john_doe", "securepass") is None

    def test_update_user(self, user_service, registered_user):
        """Test updating user"""
        updated_user = user_service.update_user(
            registered_user.id,
            email="newemail@example.com"
        )

        assert updated_user is not None
        assert updated_user.email == "newemail@example.com"

    def test_update_nonexistent_user(self, user_service):
        """Test updating nonexistent user"""
        result = user_service.update_user(999, email="test@example.com")
        assert result is None

    def test_deactivate_user(self, user_service, registered_user):
        """Test deactivating user"""
        assert user_service.deactivate_user(registered_user.id) is True

        # Verify user is deactivated
        user = user_service.get_user(registered_user.id)
        assert user.is_active is False

    def test_deactivate_nonexistent_user(self, user_service):
        """Test deactivating nonexistent user"""
        assert user_service.deactivate_user(999) is False

Integration Test Examples

# tests/integration/test_user_workflow.py
import pytest
from src.services.user_service import UserRepository, UserService

class TestUserWorkflow:
    """User workflow integration tests"""

    @pytest.fixture
    def clean_service(self):
        """Use fresh service for each test"""
        repository = UserRepository()
        return UserService(repository)

    def test_complete_user_lifecycle(self, clean_service):
        """Test complete user lifecycle"""
        service = clean_service

        # 1. Register user
        user = service.register_user("lifecycleuser", "lifecycle@example.com", "password123")
        assert user.id is not None
        original_id = user.id

        # 2. Authenticate user
        authenticated = service.authenticate("lifecycleuser", "password123")
        assert authenticated is not None
        assert authenticated.id == original_id

        # 3. Update user information
        updated = service.update_user(original_id, email="updated@example.com")
        assert updated.email == "updated@example.com"

        # 4. Verify authentication still works after update
        auth_after_update = service.authenticate("lifecycleuser", "password123")
        assert auth_after_update is not None
        assert auth_after_update.email == "updated@example.com"

        # 5. Deactivate user
        assert service.deactivate_user(original_id) is True

        # 6. Verify deactivated user cannot authenticate
        auth_after_deactivate = service.authenticate("lifecycleuser", "password123")
        assert auth_after_deactivate is None

        # 7. Verify can still get deactivated user information
        deactivated_user = service.get_user(original_id)
        assert deactivated_user is not None
        assert deactivated_user.is_active is False

    def test_multiple_users_interaction(self, clean_service):
        """Test multiple user interactions"""
        service = clean_service

        # Register multiple users
        users = []
        for i in range(3):
            user = service.register_user(f"user{i}", f"user{i}@example.com", f"pass{i}")
            users.append(user)

        # Verify all users can authenticate independently
        for i, user in enumerate(users):
            authenticated = service.authenticate(f"user{i}", f"pass{i}")
            assert authenticated is not None
            assert authenticated.id == user.id

        # Verify authentication isolation between users
        assert service.authenticate("user0", "pass1") is None
        assert service.authenticate("user1", "pass0") is None

        # Deactivating one user doesn't affect others
        service.deactivate_user(users[0].id)
        assert service.authenticate("user0", "pass0") is None
        assert service.authenticate("user1", "pass1") is not None
        assert service.authenticate("user2", "pass2") is not None

    def test_edge_cases_workflow(self, clean_service):
        """Test edge cases workflow"""
        service = clean_service

        # Try to authenticate nonexistent user
        assert service.authenticate("nonexistent", "anypass") is None

        # Try to register duplicate username immediately after registration
        service.register_user("edgeuser", "edge@example.com", "password")

        with pytest.raises(ValueError):
            service.register_user("edgeuser", "different@example.com", "password")

        # Update nonexistent user
        assert service.update_user(999, email="test@example.com") is None

        # Deactivate nonexistent user
        assert service.deactivate_user(999) is False

Test-Driven Development (TDD) Practices

TDD Red-Green-Refactor Cycle

# Example: TDD development of password strength validation feature

# 1. Red phase: Write failing test
def test_password_strength_weak():
    """Test weak password"""
    from src.utils.password_validator import PasswordValidator

    validator = PasswordValidator()
    assert validator.check_strength("123") == "weak"

# 2. Green phase: Write simplest implementation
# src/utils/password_validator.py
class PasswordValidator:
    def check_strength(self, password):
        return "weak"  # Simplest implementation

# 3. Refactor phase: Improve implementation
class PasswordValidator:
    def check_strength(self, password):
        if len(password) < 6:
            return "weak"
        elif len(password) < 10:
            return "medium"
        else:
            return "strong"

# 4. Add more tests
@pytest.mark.parametrize("password,expected", [
    ("123", "weak"),
    ("12345", "weak"),
    ("password", "medium"),
    ("verylongpassword", "strong"),
    ("P@ssw0rd123", "strong")
])
def test_password_strength_levels(password, expected):
    validator = PasswordValidator()
    assert validator.check_strength(password) == expected

Best Practices Summary

Project Organization Best Practices

  1. Clear directory structure

    tests/
    ├── unit/           # Unit tests
    ├── integration/    # Integration tests
    ├── e2e/           # End-to-end tests
    ├── fixtures/      # Test data
    └── conftest.py    # Shared configuration
  2. Standardized configuration files

    # pytest.ini
    [tool:pytest]
    testpaths = tests
    python_files = test_*.py *_test.py
    python_functions = test_*
    python_classes = Test*
    addopts = -v --tb=short --strict-markers
    markers =
        unit: Unit tests
        integration: Integration tests
        slow: Slow tests
  3. Dependency management

    # requirements-test.txt
    pytest>=7.0.0
    pytest-cov>=4.0.0
    pytest-mock>=3.0.0
    pytest-xdist>=3.0.0
    pytest-html>=3.0.0

Code Quality Assurance

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
      - id: black
  - repo: https://github.com/pycqa/flake8
    rev: 4.0.1
    hooks:
      - id: flake8
  - repo: local
    hooks:
      - id: pytest-check
        name: pytest-check
        entry: pytest
        language: system
        pass_filenames: false
        always_run: true

CI/CD Integration Example

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, "3.10", "3.11"]

    steps:
    - uses: actions/checkout@v3

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install -r requirements-test.txt

    - name: Run tests
      run: |
        pytest --cov=src --cov-report=xml --cov-report=html

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
Best Practices Key Points
  1. Testing pyramid: 70% unit tests, 20% integration tests, 10% E2E tests
  2. Fast feedback: Unit tests should complete in seconds
  3. Independence: Each test should be able to run independently
  4. Readability: Test code should be as readable as documentation
  5. Maintainability: Regularly refactor and update test code
Common Pitfalls
  1. Over-testing: Don’t write meaningless tests just for coverage
  2. Fragile tests: Avoid tests that depend too much on implementation details
  3. Slow tests: Don’t let the test suite become too slow
  4. Test pollution: Ensure tests don’t interfere with each other

By following these best practices, you can build a robust, maintainable, and efficient test suite, laying a solid foundation for the long-term success of your project.