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
-
Clear directory structure
tests/ ├── unit/ # Unit tests ├── integration/ # Integration tests ├── e2e/ # End-to-end tests ├── fixtures/ # Test data └── conftest.py # Shared configuration -
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 -
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
- Testing pyramid: 70% unit tests, 20% integration tests, 10% E2E tests
- Fast feedback: Unit tests should complete in seconds
- Independence: Each test should be able to run independently
- Readability: Test code should be as readable as documentation
- Maintainability: Regularly refactor and update test code
Common Pitfalls
- Over-testing: Don’t write meaningless tests just for coverage
- Fragile tests: Avoid tests that depend too much on implementation details
- Slow tests: Don’t let the test suite become too slow
- 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.