python-testing-standards
π―Skillfrom clostaunau/holiday-card
Enforces Python testing best practices, providing comprehensive guidelines for writing high-quality, maintainable pytest test suites with code coverage and mocking strategies.
Installation
npx skills add https://github.com/clostaunau/holiday-card --skill python-testing-standardsSkill Details
Comprehensive Python testing best practices, pytest conventions, test structure patterns (AAA, Given-When-Then), fixture usage, mocking strategies, code coverage standards, and common anti-patterns. Essential reference for code reviews, test writing, and ensuring high-quality Python test suites with pytest, unittest.mock, and pytest-cov.
Overview
# Python Testing Standards
Purpose
This skill provides comprehensive testing standards and best practices for Python projects using pytest. It serves as a reference guide during code reviews to ensure test quality, maintainability, and adherence to Python testing conventions.
When to use this skill:
- Conducting code reviews of Python test files
- Writing new test suites
- Refactoring existing tests
- Evaluating test coverage and quality
- Teaching testing best practices to team members
Context
High-quality tests are essential for maintaining reliable Python applications. This skill documents industry-standard testing practices using pytest, the de facto testing framework for modern Python development. These standards emphasize:
- Clarity: Tests should be easy to read and understand
- Maintainability: Tests should be easy to update as code evolves
- Reliability: Tests should be deterministic and fast
- Coverage: Tests should cover behavior, not just lines of code
- Isolation: Tests should be independent and not affect each other
This skill is designed to be referenced by the uncle-duke-python agent during code reviews and by developers when writing tests.
Prerequisites
Required Knowledge:
- Python fundamentals
- Basic understanding of testing concepts
- Familiarity with pytest (or willingness to learn)
Required Tools:
- pytest (
pip install pytest) - pytest-cov for coverage (
pip install pytest-cov) - pytest-mock for mocking (
pip install pytest-mock) - unittest.mock (built into Python 3.3+)
Expected Project Structure:
```
project/
βββ src/
β βββ mypackage/
β βββ __init__.py
β βββ module.py
βββ tests/
β βββ __init__.py
β βββ conftest.py
β βββ unit/
β β βββ test_module.py
β βββ integration/
β βββ test_integration.py
βββ pytest.ini
βββ requirements-dev.txt
```
---
Instructions
Task 1: Verify pytest Conventions
#### 1.1 Test File Naming
Rule: Test files MUST follow one of these patterns:
test_*.py(preferred)*_test.py
Examples:
β Good:
```python
# File: test_user_service.py
# File: test_authentication.py
# File: user_service_test.py # acceptable but less common
```
β Bad:
```python
# File: user_tests.py # missing 'test_' prefix
# File: test-user-service.py # hyphens instead of underscores
# File: TestUserService.py # CamelCase instead of snake_case
```
Why: pytest discovers test files by these patterns. Non-standard names won't be automatically discovered.
#### 1.2 Test Function Naming
Rule: Test functions MUST start with test_ and describe the scenario being tested.
Pattern: test_
β Good:
```python
def test_user_login_with_valid_credentials_returns_token():
"""Test that valid credentials return an auth token."""
pass
def test_user_login_with_invalid_password_raises_authentication_error():
"""Test that invalid password raises AuthenticationError."""
pass
def test_calculate_total_with_empty_cart_returns_zero():
"""Test that empty cart returns zero total."""
pass
```
β Bad:
```python
def test_login(): # Too vague
pass
def test_user_service(): # Doesn't describe what's being tested
pass
def testLogin(): # Missing underscore, CamelCase
pass
def test_1(): # Non-descriptive
pass
```
Why: Descriptive test names serve as documentation. When a test fails, the name should immediately tell you what broke.
#### 1.3 Test Class Naming
Rule: Test classes MUST start with Test (no _test suffix).
β Good:
```python
class TestUserAuthentication:
"""Tests for user authentication functionality."""
def test_login_with_valid_credentials_succeeds(self):
pass
def test_login_with_invalid_credentials_fails(self):
pass
class TestShoppingCart:
"""Tests for shopping cart operations."""
def test_add_item_increases_count(self):
pass
```
β Bad:
```python
class UserAuthenticationTests: # Missing 'Test' prefix
pass
class Test_UserAuth: # Unnecessary underscore
pass
class testUserAuth: # Lowercase 'test'
pass
```
Why: pytest discovers test classes by the Test prefix. Classes provide logical grouping of related tests.
When to use classes vs functions:
- Use classes when tests share setup/teardown logic or fixtures
- Use functions for simple, independent tests
- Don't use classes just for grouping without shared behavior
#### 1.4 Test Organization and Structure
Directory Structure:
```
tests/
βββ __init__.py # Makes tests a package
βββ conftest.py # Shared fixtures and configuration
βββ unit/ # Unit tests (fast, isolated)
β βββ __init__.py
β βββ test_models.py
β βββ test_services.py
β βββ test_utils.py
βββ integration/ # Integration tests (slower, external deps)
β βββ __init__.py
β βββ test_database.py
β βββ test_api_endpoints.py
βββ e2e/ # End-to-end tests (slowest)
βββ __init__.py
βββ test_user_workflows.py
```
File Organization:
Within a test file, organize tests logically:
```python
"""Tests for user authentication service."""
import pytest
from myapp.auth import AuthenticationService
# Fixtures at the top
@pytest.fixture
def auth_service():
"""Fixture providing authentication service instance."""
return AuthenticationService()
# Test classes grouped by functionality
class TestUserLogin:
"""Tests for user login functionality."""
def test_login_with_valid_credentials_succeeds(self, auth_service):
pass
def test_login_with_invalid_password_fails(self, auth_service):
pass
class TestUserLogout:
"""Tests for user logout functionality."""
def test_logout_invalidates_session(self, auth_service):
pass
# Standalone functions for simple tests
def test_password_hashing_is_deterministic():
"""Test that same password always produces same hash."""
pass
```
Task 2: Apply Test Structure Patterns
#### 2.1 Arrange-Act-Assert (AAA) Pattern
Rule: Structure all tests using the AAA pattern with clear separation.
Pattern:
- Arrange: Set up test data and preconditions
- Act: Execute the behavior being tested
- Assert: Verify the expected outcome
β Good:
```python
def test_calculate_total_with_discount_applies_correctly():
"""Test that discount is correctly applied to cart total."""
# Arrange
cart = ShoppingCart()
cart.add_item(Item(name="Widget", price=100.00))
cart.add_item(Item(name="Gadget", price=50.00))
discount = Discount(percentage=10)
# Act
total = cart.calculate_total(discount=discount)
# Assert
assert total == 135.00 # (100 + 50) * 0.9
```
β Good (with comments):
```python
def test_user_registration_creates_new_user():
"""Test that user registration creates a new user record."""
# Arrange
user_data = {
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123!"
}
user_service = UserService()
# Act
user = user_service.register(user_data)
# Assert
assert user.username == "testuser"
assert user.email == "test@example.com"
assert user.id is not None
assert user.password_hash != user_data["password"] # Hashed
```
β Bad:
```python
def test_user_operations():
# Everything mixed together
user_service = UserService()
user = user_service.register({"username": "test", "email": "test@example.com"})
assert user.username == "test"
user.update_email("new@example.com")
assert user.email == "new@example.com"
user_service.delete(user.id)
assert user_service.get(user.id) is None
```
Why bad: Tests multiple behaviors, hard to debug when it fails, violates single responsibility.
#### 2.2 Given-When-Then (BDD Pattern)
Alternative to AAA: Given-When-Then is equivalent but uses BDD terminology.
Pattern:
- Given: Preconditions and setup (same as Arrange)
- When: Action being tested (same as Act)
- Then: Expected outcome (same as Assert)
β Good:
```python
def test_withdraw_with_sufficient_balance_succeeds():
"""
Given an account with a balance of $100
When withdrawing $30
Then the withdrawal succeeds and balance is $70
"""
# Given
account = Account(balance=100.00)
# When
result = account.withdraw(30.00)
# Then
assert result is True
assert account.balance == 70.00
```
Usage: Use Given-When-Then for behavior-driven tests, especially in integration/e2e tests. Use AAA for unit tests. Be consistent within a project.
#### 2.3 Test Isolation
Rule: Each test MUST be completely independent and not rely on execution order.
β Good:
```python
@pytest.fixture
def clean_database():
"""Provide a clean database for each test."""
db = Database()
db.clear()
yield db
db.clear() # Cleanup after test
def test_create_user_adds_to_database(clean_database):
"""Test user creation adds record to database."""
user = User(username="test")
clean_database.save(user)
assert clean_database.count() == 1
def test_delete_user_removes_from_database(clean_database):
"""Test user deletion removes record from database."""
user = User(username="test")
clean_database.save(user)
clean_database.delete(user.id)
assert clean_database.count() == 0
```
β Bad:
```python
# Tests depend on execution order
db = Database()
def test_1_create_user():
"""This test must run first."""
user = User(username="test")
db.save(user)
assert db.count() == 1
def test_2_delete_user():
"""This test depends on test_1 running first."""
# Assumes user from test_1 exists
users = db.get_all()
db.delete(users[0].id)
assert db.count() == 0
```
Why bad: Tests are brittle, fail when run in isolation or different order, hard to debug.
#### 2.4 Test Data Management
Rule: Use fixtures, factories, or builders for test data. Avoid magic values.
β Good:
```python
@pytest.fixture
def sample_user():
"""Provide a sample user for testing."""
return User(
username="john_doe",
email="john@example.com",
age=30,
is_active=True
)
@pytest.fixture
def user_factory():
"""Provide a factory for creating test users."""
def _create_user(**kwargs):
defaults = {
"username": "test_user",
"email": "test@example.com",
"age": 25,
"is_active": True
}
defaults.update(kwargs)
return User(**defaults)
return _create_user
def test_user_validation_with_invalid_age(user_factory):
"""Test that invalid age raises validation error."""
user = user_factory(age=-5)
with pytest.raises(ValidationError):
user.validate()
```
β Bad:
```python
def test_user_creation():
# Magic values scattered throughout
user = User("john", "john@example.com", 30, True)
assert user.username == "john"
def test_user_update():
# Same magic values repeated
user = User("john", "john@example.com", 30, True)
user.update_age(31)
assert user.age == 31
```
Why bad: Hard to maintain, unclear what values mean, violates DRY principle.
Task 3: Implement Fixture Best Practices
#### 3.1 Fixture Scopes
Rule: Choose the appropriate scope based on fixture cost and state.
Available Scopes:
function: Default, runs before each test function (most common)class: Runs once per test classmodule: Runs once per modulesession: Runs once per test session
β Good:
```python
@pytest.fixture(scope="session")
def database_engine():
"""Create database engine once per test session."""
engine = create_engine("postgresql://localhost/test_db")
yield engine
engine.dispose()
@pytest.fixture(scope="module")
def database_schema(database_engine):
"""Create database schema once per module."""
Base.metadata.create_all(database_engine)
yield
Base.metadata.drop_all(database_engine)
@pytest.fixture(scope="function")
def database_session(database_engine):
"""Provide a clean database session for each test."""
connection = database_engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
```
Scope Selection Guidelines:
- Use
functionscope for fixtures that need to be fresh for each test - Use
moduleorsessionscope for expensive setup (database connections, API clients) - NEVER use broader scope for fixtures that maintain state between tests
- Always clean up in broader-scoped fixtures
β Bad:
```python
@pytest.fixture(scope="session")
def user_list():
"""DON'T DO THIS - mutable state in session scope."""
return [] # This list will be shared across ALL tests!
def test_add_user(user_list):
user_list.append("user1")
assert len(user_list) == 1 # Might fail if other tests run first
def test_remove_user(user_list):
# Depends on state from other tests
user_list.remove("user1")
```
Why bad: Shared mutable state causes tests to interfere with each other.
#### 3.2 Fixture Dependencies
Rule: Fixtures can depend on other fixtures to build complex test scenarios.
β Good:
```python
@pytest.fixture
def database():
"""Provide database connection."""
db = Database("test.db")
yield db
db.close()
@pytest.fixture
def user(database):
"""Provide a test user in the database."""
user = User(username="testuser")
database.save(user)
return user
@pytest.fixture
def authenticated_user(user):
"""Provide an authenticated user."""
user.login()
return user
def test_user_can_access_profile(authenticated_user, database):
"""Test authenticated user can access their profile."""
profile = database.get_profile(authenticated_user.id)
assert profile is not None
assert profile.user_id == authenticated_user.id
```
Dependency Chain: database β user β authenticated_user
This approach builds complex test scenarios from simple, reusable fixtures.
#### 3.3 Fixture Naming Conventions
Rule: Fixture names should be clear, descriptive nouns or noun phrases.
β Good:
```python
@pytest.fixture
def database_session():
"""Provide a database session."""
pass
@pytest.fixture
def mock_email_service():
"""Provide a mocked email service."""
pass
@pytest.fixture
def sample_user_data():
"""Provide sample user data dictionary."""
pass
@pytest.fixture
def http_client():
"""Provide an HTTP client for API testing."""
pass
```
β Bad:
```python
@pytest.fixture
def get_db(): # Verb, not noun
pass
@pytest.fixture
def data(): # Too vague
pass
@pytest.fixture
def fixture1(): # Non-descriptive
pass
@pytest.fixture
def temp(): # Unclear what it provides
pass
```
#### 3.4 Fixtures vs Helper Functions
Rule: Use fixtures for setup/teardown and state. Use helper functions for operations.
β Good:
```python
# Fixture for state/setup
@pytest.fixture
def user():
"""Provide a test user."""
return User(username="test")
# Helper function for operations
def create_post(user, title, content):
"""Helper to create a post for testing."""
return Post(author=user, title=title, content=content)
def test_user_can_create_post(user):
"""Test that user can create a post."""
post = create_post(user, "Test Title", "Test Content")
assert post.author == user
assert post.title == "Test Title"
```
When to use fixtures:
- Setting up test data or objects
- Managing resources (database connections, file handles)
- Setup/teardown logic
- Sharing state across multiple tests
When to use helper functions:
- Performing operations in tests
- Reducing code duplication in test bodies
- Complex assertions
- Test data generation with many parameters
#### 3.5 autouse Fixtures
Rule: Use autouse=True sparingly, only for fixtures that should always run.
β Good:
```python
@pytest.fixture(autouse=True)
def reset_global_state():
"""Reset global application state before each test."""
AppConfig.reset()
Cache.clear()
yield
# Cleanup after test
@pytest.fixture(autouse=True, scope="session")
def configure_logging():
"""Configure logging for test session."""
logging.basicConfig(level=logging.DEBUG)
```
β Bad:
```python
@pytest.fixture(autouse=True)
def create_test_user():
"""DON'T DO THIS - not all tests need a user."""
return User(username="test") # Wastes resources for tests that don't need it
```
Use autouse when:
- Resetting global state
- Configuring logging/warnings
- Setting up test environment variables
- Cleanup that should always happen
Don't use autouse when:
- Only some tests need the fixture
- The fixture is expensive to create
- It makes test dependencies unclear
#### 3.6 conftest.py Best Practices
Rule: Place shared fixtures in conftest.py at appropriate levels.
Directory Structure:
```
tests/
βββ conftest.py # Shared across ALL tests
βββ unit/
β βββ conftest.py # Shared across unit tests only
β βββ test_services.py
βββ integration/
βββ conftest.py # Shared across integration tests only
βββ test_database.py
```
Example conftest.py:
```python
"""Shared fixtures for all tests."""
import pytest
from myapp import create_app
from myapp.database import Database
@pytest.fixture(scope="session")
def app():
"""Provide application instance for testing."""
app = create_app(testing=True)
return app
@pytest.fixture
def client(app):
"""Provide test client for making HTTP requests."""
return app.test_client()
@pytest.fixture
def database():
"""Provide clean database for testing."""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
# Register custom markers
def pytest_configure(config):
"""Register custom markers."""
config.addinivalue_line(
"markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
)
config.addinivalue_line(
"markers", "integration: marks tests as integration tests"
)
```
Task 4: Apply Parametrization
Rule: Use @pytest.mark.parametrize to test multiple inputs without duplicating code.
β Good:
```python
@pytest.mark.parametrize("value,expected", [
(0, False),
(1, True),
(42, True),
(-1, True),
(-42, True),
])
def test_is_non_zero(value, expected):
"""Test is_non_zero function with various inputs."""
assert is_non_zero(value) == expected
```
Multiple Parameters:
```python
@pytest.mark.parametrize("username,email,valid", [
("john_doe", "john@example.com", True),
("", "john@example.com", False), # Empty username
("john_doe", "", False), # Empty email
("john_doe", "invalid-email", False), # Invalid email format
("ab", "short@example.com", False), # Username too short
])
def test_user_validation(username, email, valid):
"""Test user validation with various inputs."""
user = User(username=username, email=email)
if valid:
user.validate() # Should not raise
else:
with pytest.raises(ValidationError):
user.validate()
```
Using pytest.param for Test IDs:
```python
@pytest.mark.parametrize("input_data,expected", [
pytest.param(
{"username": "john", "age": 30},
User(username="john", age=30),
id="valid_user"
),
pytest.param(
{"username": "", "age": 30},
ValidationError,
id="empty_username"
),
pytest.param(
{"username": "john", "age": -5},
ValidationError,
id="negative_age"
),
])
def test_user_creation(input_data, expected):
"""Test user creation with various inputs."""
if isinstance(expected, type) and issubclass(expected, Exception):
with pytest.raises(expected):
User(**input_data)
else:
user = User(**input_data)
assert user.username == expected.username
assert user.age == expected.age
```
Parametrizing Fixtures:
```python
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database(request):
"""Provide different database backends for testing."""
db_type = request.param
if db_type == "sqlite":
db = SQLiteDatabase(":memory:")
elif db_type == "postgresql":
db = PostgreSQLDatabase("localhost", "test_db")
elif db_type == "mysql":
db = MySQLDatabase("localhost", "test_db")
db.create_schema()
yield db
db.drop_schema()
db.close()
def test_database_insert(database):
"""Test insert works on all database backends."""
# This test runs 3 times, once for each database type
database.insert("users", {"username": "test"})
assert database.count("users") == 1
```
Task 5: Implement Mocking and Patching Best Practices
#### 5.1 When to Mock vs When Not to Mock
Mock:
- External services (APIs, databases, email services)
- Slow operations (file I/O, network calls)
- Non-deterministic operations (random, datetime)
- Side effects you want to verify (logging, events)
Don't Mock:
- Simple data structures (dictionaries, lists)
- Pure functions with no side effects
- Your own internal business logic (test it directly)
- Value objects and entities
β Good (mocking external service):
```python
def test_send_welcome_email_calls_email_service(mocker):
"""Test that user registration sends welcome email."""
# Mock external email service
mock_email = mocker.patch("myapp.services.EmailService.send")
user_service = UserService()
user = user_service.register("john@example.com")
# Verify email service was called
mock_email.assert_called_once_with(
to="john@example.com",
subject="Welcome!",
template="welcome"
)
```
β Bad (mocking internal logic):
```python
def test_calculate_total(mocker):
"""DON'T DO THIS - mocking the thing you're testing."""
cart = ShoppingCart()
# Mocking internal method defeats the purpose of testing
mocker.patch.object(cart, "calculate_total", return_value=100.0)
assert cart.calculate_total() == 100.0 # Pointless test
```
#### 5.2 unittest.mock Usage
Basic Mocking:
```python
from unittest.mock import Mock, MagicMock, patch
def test_user_service_with_mock_database():
"""Test user service with mocked database."""
# Create a mock database
mock_db = Mock()
mock_db.save.return_value = True
mock_db.get.return_value = User(id=1, username="test")
user_service = UserService(database=mock_db)
user = user_service.create_user("test")
# Verify interactions
mock_db.save.assert_called_once()
assert user.username == "test"
```
Using MagicMock for Magic Methods:
```python
def test_context_manager():
"""Test code that uses context managers."""
mock_file = MagicMock()
mock_file.__enter__.return_value = mock_file
mock_file.read.return_value = "test content"
with mock_file as f:
content = f.read()
assert content == "test content"
mock_file.__enter__.assert_called_once()
mock_file.__exit__.assert_called_once()
```
#### 5.3 pytest-mock Plugin
Preferred Approach: Use pytest-mock's mocker fixture for cleaner syntax.
β Good:
```python
def test_get_user_data_from_api(mocker):
"""Test fetching user data from external API."""
# Mock the requests.get call
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {
"id": 1,
"name": "John Doe"
}
mock_get.return_value.status_code = 200
api_client = APIClient()
user_data = api_client.get_user(user_id=1)
# Verify API was called correctly
mock_get.assert_called_once_with(
"https://api.example.com/users/1",
headers={"Authorization": "Bearer token"}
)
assert user_data["name"] == "John Doe"
```
Mocking Class Methods:
```python
def test_service_calls_repository(mocker):
"""Test that service layer calls repository correctly."""
# Mock the repository method
mock_find = mocker.patch("myapp.repositories.UserRepository.find_by_email")
mock_find.return_value = User(id=1, email="test@example.com")
service = UserService()
user = service.get_user_by_email("test@example.com")
mock_find.assert_called_once_with("test@example.com")
assert user.email == "test@example.com"
```
#### 5.4 Patching Best Practices
Rule: Patch where the object is used, not where it's defined.
β Good:
```python
# myapp/services.py
from myapp.repositories import UserRepository
class UserService:
def get_user(self, user_id):
return UserRepository.find(user_id)
# tests/test_services.py
def test_get_user(mocker):
"""Patch where UserRepository is USED."""
# Correct: patch in myapp.services where it's imported
mock_find = mocker.patch("myapp.services.UserRepository.find")
mock_find.return_value = User(id=1)
service = UserService()
user = service.get_user(1)
assert user.id == 1
```
β Bad:
```python
def test_get_user(mocker):
"""DON'T DO THIS - patching where it's defined."""
# Wrong: patch in myapp.repositories where it's defined
mock_find = mocker.patch("myapp.repositories.UserRepository.find")
# This won't work because UserService imported it before the patch
```
Patching Built-ins:
```python
def test_file_processing(mocker):
"""Test file processing with mocked open."""
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="test data"))
processor = FileProcessor()
content = processor.read_file("test.txt")
mock_open.assert_called_once_with("test.txt", "r")
assert content == "test data"
```
Patching datetime:
```python
from datetime import datetime
def test_timestamp_generation(mocker):
"""Test timestamp generation with frozen time."""
# Mock datetime.now()
mock_datetime = mocker.patch("myapp.utils.datetime")
mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0)
timestamp = generate_timestamp()
assert timestamp == "2024-01-01 12:00:00"
```
#### 5.5 Mock Assertions
Rule: Always verify that mocks were called correctly.
Common Assertions:
```python
def test_mock_assertions(mocker):
"""Demonstrate common mock assertions."""
mock_service = mocker.Mock()
# Call the mock
mock_service.send_email("test@example.com", "Hello")
mock_service.send_email("other@example.com", "World")
# Verify it was called
assert mock_service.send_email.called
assert mock_service.send_email.call_count == 2
# Verify specific calls
mock_service.send_email.assert_called_with("other@example.com", "World")
mock_service.send_email.assert_any_call("test@example.com", "Hello")
# Verify all calls
assert mock_service.send_email.call_args_list == [
mocker.call("test@example.com", "Hello"),
mocker.call("other@example.com", "World"),
]
```
Verify Mock Not Called:
```python
def test_service_does_not_send_email_for_inactive_users(mocker):
"""Test that inactive users don't receive emails."""
mock_email = mocker.patch("myapp.services.EmailService.send")
user_service = UserService()
user_service.notify_user(User(is_active=False))
# Verify email was NOT sent
mock_email.assert_not_called()
```
Side Effects:
```python
def test_retry_logic_with_failures(mocker):
"""Test retry logic when API calls fail."""
mock_api = mocker.Mock()
# First two calls raise exception, third succeeds
mock_api.fetch.side_effect = [
ConnectionError("Failed"),
ConnectionError("Failed"),
{"status": "success"}
]
client = APIClient(api=mock_api)
result = client.fetch_with_retry(max_retries=3)
assert result == {"status": "success"}
assert mock_api.fetch.call_count == 3
```
Task 6: Implement Code Coverage Best Practices
#### 6.1 Coverage Thresholds
Recommended Thresholds:
- Overall project: 80% minimum
- New code: 90% minimum (enforce in CI)
- Critical paths: 100% (authentication, payment, security)
pytest.ini Configuration:
```ini
[pytest]
# Run with coverage by default in CI
addopts = --cov=myapp --cov-report=html --cov-report=term --cov-fail-under=80
# Coverage options
[coverage:run]
omit =
/tests/
/migrations/
/venv/
/__pycache__/
/site-packages/
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
```
Running Coverage:
```bash
# Generate coverage report
pytest --cov=myapp --cov-report=html --cov-report=term
# View HTML report
open htmlcov/index.html
# Fail if coverage below threshold
pytest --cov=myapp --cov-fail-under=80
# Show missing lines
pytest --cov=myapp --cov-report=term-missing
```
#### 6.2 What to Test (and What Not to Test)
DO Test:
β Business Logic:
```python
def calculate_discount(price, customer_tier):
"""Calculate discount based on customer tier."""
if customer_tier == "gold":
return price * 0.20
elif customer_tier == "silver":
return price * 0.10
return 0.0
# TEST THIS - it's core business logic
def test_calculate_discount_for_gold_tier():
assert calculate_discount(100.0, "gold") == 20.0
```
β Edge Cases and Boundaries:
```python
def test_get_user_with_nonexistent_id_raises_error():
"""Test that fetching non-existent user raises NotFound."""
with pytest.raises(NotFoundError):
user_service.get_user(user_id=999999)
def test_divide_by_zero_raises_error():
"""Test division by zero raises appropriate error."""
with pytest.raises(ZeroDivisionError):
calculator.divide(10, 0)
```
β Error Handling:
```python
def test_invalid_email_format_raises_validation_error():
"""Test that invalid email format raises ValidationError."""
with pytest.raises(ValidationError, match="Invalid email format"):
User(email="not-an-email")
```
β Public API/Interface:
```python
class UserService:
def register(self, email, password): # Public API - TEST
"""Register a new user."""
user = self._create_user(email) # Private - don't test directly
self._send_welcome_email(user) # Private - don't test directly
return user
# Test the public method, not private helpers
def test_register_creates_user_and_sends_email(mocker):
mock_email = mocker.patch("myapp.services.EmailService.send")
user = user_service.register("test@example.com", "password")
assert user.email == "test@example.com"
mock_email.assert_called_once()
```
DON'T Test:
β Framework/Library Code:
```python
# DON'T test that Django's ORM works
def test_user_save():
"""DON'T DO THIS - testing Django, not your code."""
user = User(username="test")
user.save()
assert User.objects.filter(username="test").exists()
```
β Trivial Getters/Setters:
```python
class User:
@property
def username(self):
return self._username # DON'T test this
@username.setter
def username(self, value):
self._username = value # DON'T test this
```
β Private Implementation Details:
```python
class Calculator:
def add(self, a, b):
return self._perform_addition(a, b)
def _perform_addition(self, a, b): # Private method
return a + b
# DON'T test _perform_addition directly
# Test the public add() method instead
```
β Generated Code:
```python
# DON'T test auto-generated migration files, __init__.py, etc.
```
#### 6.3 Coverage Tools
pytest-cov:
```bash
# Install
pip install pytest-cov
# Basic usage
pytest --cov=myapp
# With HTML report
pytest --cov=myapp --cov-report=html
# Show missing lines
pytest --cov=myapp --cov-report=term-missing
# Multiple formats
pytest --cov=myapp --cov-report=html --cov-report=term --cov-report=xml
```
Coverage.py (underlying tool):
```bash
# Run tests with coverage
coverage run -m pytest
# Generate report
coverage report
# HTML report
coverage html
# Combine coverage from multiple runs
coverage combine
coverage report
```
Branch Coverage:
```ini
[coverage:run]
branch = True # Enable branch coverage (not just line coverage)
```
```python
def example(x):
if x > 0: # Branch 1
return "positive"
else: # Branch 2
return "non-positive"
# Line coverage: 100% if you test with x=1
# Branch coverage: 50% - you need to test both x>0 and x<=0
```
Task 7: Avoid Common Anti-Patterns
#### 7.1 Anti-Pattern: Testing Implementation Details
β Bad:
```python
def test_user_service_implementation_details():
"""DON'T DO THIS - testing how it works, not what it does."""
service = UserService()
# Testing that internal _validate method is called
with patch.object(service, "_validate") as mock_validate:
service.register("test@example.com")
mock_validate.assert_called_once()
```
β Good:
```python
def test_user_service_validates_email():
"""Test the behavior: invalid emails are rejected."""
service = UserService()
# Test the behavior, not the implementation
with pytest.raises(ValidationError):
service.register("invalid-email")
```
Why: Implementation details change. Tests should verify behavior, not how the behavior is achieved.
#### 7.2 Anti-Pattern: Overly Complex Test Setup
β Bad:
```python
def test_complex_scenario():
"""DON'T DO THIS - setup is too complex."""
# 50 lines of setup code
db = Database()
db.connect()
db.create_tables()
user1 = User(username="user1")
user2 = User(username="user2")
db.save(user1)
db.save(user2)
post1 = Post(author=user1, title="Post 1")
post2 = Post(author=user2, title="Post 2")
db.save(post1)
db.save(post2)
comment1 = Comment(post=post1, author=user2, text="Comment")
db.save(comment1)
# ... many more lines
# Finally, the actual test
result = service.get_posts()
assert len(result) == 2
```
β Good:
```python
@pytest.fixture
def database_with_posts():
"""Provide database with test posts."""
db = Database()
db.create_schema()
users = [User(username=f"user{i}") for i in range(2)]
posts = [Post(author=users[i], title=f"Post {i}") for i in range(2)]
for user in users:
db.save(user)
for post in posts:
db.save(post)
yield db
db.close()
def test_get_posts(database_with_posts):
"""Test retrieving posts."""
result = service.get_posts()
assert len(result) == 2
```
Why: Complex setup makes tests hard to read and maintain. Use fixtures and factories.
#### 7.3 Anti-Pattern: Flaky Tests
β Bad:
```python
import time
import random
def test_flaky_timing():
"""DON'T DO THIS - test depends on timing."""
start = time.time()
process_data()
duration = time.time() - start
# Flaky: might fail on slow systems
assert duration < 1.0
def test_flaky_random():
"""DON'T DO THIS - test uses uncontrolled randomness."""
result = generate_random_value()
assert result > 5 # Might fail randomly
def test_flaky_order():
"""DON'T DO THIS - test depends on iteration order."""
users = User.objects.all() # Order not guaranteed
assert users[0].username == "alice"
```
β Good:
```python
def test_with_mocked_time(mocker):
"""Test with controlled time."""
mock_time = mocker.patch("time.time")
mock_time.side_effect = [0.0, 0.5] # Controlled values
start = time.time()
process_data()
duration = time.time() - start
assert duration == 0.5
def test_with_seeded_random(mocker):
"""Test with controlled randomness."""
mocker.patch("random.randint", return_value=7)
result = generate_random_value()
assert result == 7
def test_with_explicit_order():
"""Test with guaranteed order."""
users = User.objects.all().order_by("username")
assert users[0].username == "alice"
```
Why: Flaky tests erode trust in the test suite and waste developer time.
#### 7.4 Anti-Pattern: Too Many Assertions in One Test
β Bad:
```python
def test_user_operations():
"""DON'T DO THIS - testing multiple behaviors."""
# Test 1: User creation
user = User(username="test", email="test@example.com")
assert user.username == "test"
assert user.email == "test@example.com"
# Test 2: User validation
user.validate()
assert user.is_valid is True
# Test 3: User saving
user.save()
assert user.id is not None
# Test 4: User updating
user.username = "updated"
user.save()
assert user.username == "updated"
# Test 5: User deletion
user.delete()
assert User.objects.filter(id=user.id).count() == 0
```
β Good:
```python
def test_user_creation():
"""Test user is created with correct attributes."""
user = User(username="test", email="test@example.com")
assert user.username == "test"
assert user.email == "test@example.com"
def test_user_validation_succeeds_for_valid_data():
"""Test validation succeeds for valid user data."""
user = User(username="test", email="test@example.com")
user.validate()
assert user.is_valid is True
def test_user_save_assigns_id():
"""Test saving user assigns an ID."""
user = User(username="test", email="test@example.com")
user.save()
assert user.id is not None
# ... separate tests for other behaviors
```
Guideline: One logical assertion per test. Multiple assert statements are okay if they verify the same behavior.
β Acceptable:
```python
def test_user_creation_sets_all_attributes():
"""Test user creation sets all required attributes."""
user = User(username="test", email="test@example.com", age=30)
# Multiple assertions, but all verify the same behavior (creation)
assert user.username == "test"
assert user.email == "test@example.com"
assert user.age == 30
assert user.is_active is True # Default value
```
#### 7.5 Anti-Pattern: Not Testing Edge Cases
β Bad:
```python
def test_divide():
"""Only tests the happy path."""
assert divide(10, 2) == 5.0
```
β Good:
```python
def test_divide_positive_numbers():
"""Test division of positive numbers."""
assert divide(10, 2) == 5.0
def test_divide_negative_numbers():
"""Test division with negative numbers."""
assert divide(-10, 2) == -5.0
assert divide(10, -2) == -5.0
def test_divide_by_zero_raises_error():
"""Test division by zero raises ZeroDivisionError."""
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_zero_by_number():
"""Test zero divided by number returns zero."""
assert divide(0, 5) == 0.0
def test_divide_floats():
"""Test division with floating point numbers."""
assert divide(10.5, 2.0) == 5.25
```
Common Edge Cases to Test:
- Empty collections ([], {}, "")
- None values
- Zero
- Negative numbers
- Very large numbers
- Boundary values (max/min)
- Invalid input types
- Special characters in strings
#### 7.6 Anti-Pattern: Missing Negative Test Cases
β Bad:
```python
def test_create_user():
"""Only tests successful creation."""
user = user_service.create("test@example.com", "password123")
assert user is not None
```
β Good:
```python
def test_create_user_with_valid_data_succeeds():
"""Test user creation succeeds with valid data."""
user = user_service.create("test@example.com", "password123")
assert user is not None
def test_create_user_with_invalid_email_raises_error():
"""Test user creation fails with invalid email."""
with pytest.raises(ValidationError):
user_service.create("invalid-email", "password123")
def test_create_user_with_short_password_raises_error():
"""Test user creation fails with short password."""
with pytest.raises(ValidationError):
user_service.create("test@example.com", "pass")
def test_create_user_with_duplicate_email_raises_error():
"""Test user creation fails with duplicate email."""
user_service.create("test@example.com", "password123")
with pytest.raises(DuplicateEmailError):
user_service.create("test@example.com", "password456")
```
Why: Negative tests verify error handling and validation logic.
---
Best Practices Summary
1. One Assertion Per Test (Guideline)
Guideline: Each test should verify one logical behavior. Multiple assert statements are acceptable if they all verify the same behavior.
β Good:
```python
def test_user_registration_creates_complete_user():
"""Test user registration creates user with all attributes."""
user = register_user("john@example.com", "password")
# All assertions verify the same behavior (successful registration)
assert user.email == "john@example.com"
assert user.is_active is True
assert user.created_at is not None
```
2. Test Names That Describe the Scenario
Pattern: test_
β Good:
```python
def test_withdraw_with_insufficient_balance_raises_error():
"""Test withdrawing more than balance raises InsufficientFundsError."""
pass
def test_login_with_valid_credentials_returns_token():
"""Test login with valid credentials returns auth token."""
pass
```
3. Fast Tests
Rule: Unit tests should run in milliseconds, entire suite in seconds.
Strategies:
- Use in-memory databases (SQLite
:memory:) - Mock external dependencies
- Use fixtures with appropriate scopes
- Avoid unnecessary I/O operations
- Run slow tests separately (
@pytest.mark.slow)
```python
@pytest.mark.slow
def test_full_database_migration():
"""Slow test - run separately with pytest -m slow."""
pass
# Run fast tests only
# pytest -m "not slow"
```
4. Independent Tests
Rule: Tests must not depend on each other or on execution order.
β Good:
```python
@pytest.fixture(autouse=True)
def reset_database():
"""Reset database before each test."""
db.clear()
yield
db.clear()
def test_create_user():
"""Each test starts with clean state."""
user = create_user("test")
assert db.count() == 1
def test_delete_user():
"""Independent of test_create_user."""
user = create_user("test")
delete_user(user.id)
assert db.count() == 0
```
5. Repeatable Tests
Rule: Tests should produce the same results every time they run.
Avoid:
- Uncontrolled randomness
- System time dependencies
- External API calls
- Filesystem dependencies
- Network dependencies
Use:
- Mocked random with fixed seed
- Mocked datetime
- Mocked external services
- Temporary directories/files
- In-memory resources
6. Self-Validating Tests
Rule: Tests should clearly pass or fail without human interpretation.
β Good:
```python
def test_calculate_total():
"""Test clearly passes or fails."""
total = calculate_total([10, 20, 30])
assert total == 60 # Clear pass/fail
```
β Bad:
```python
def test_calculate_total():
"""Requires human to interpret output."""
total = calculate_total([10, 20, 30])
print(f"Total: {total}") # No assertion - requires manual check
```
---
Examples
Example 1: Complete Test File
File: tests/unit/test_user_service.py
```python
"""Tests for user service."""
import pytest
from myapp.models import User
from myapp.services import UserService
from myapp.exceptions import ValidationError, DuplicateEmailError
@pytest.fixture
def user_service():
"""Provide user service instance."""
return UserService()
@pytest.fixture
def sample_user_data():
"""Provide sample valid user data."""
return {
"email": "john@example.com",
"password": "SecurePass123!",
"username": "john_doe"
}
class TestUserRegistration:
"""Tests for user registration functionality."""
def test_register_with_valid_data_creates_user(self, user_service, sample_user_data):
"""Test that registration with valid data creates a user."""
# Arrange
# (data provided by fixture)
# Act
user = user_service.register(**sample_user_data)
# Assert
assert user.email == sample_user_data["email"]
assert user.username == sample_user_data["username"]
assert user.id is not None
assert user.is_active is True
def test_register_with_invalid_email_raises_error(self, user_service):
"""Test that invalid email raises ValidationError."""
# Arrange
invalid_data = {
"email": "not-an-email",
"password": "SecurePass123!",
"username": "john_doe"
}
# Act & Assert
with pytest.raises(ValidationError, match="Invalid email format"):
user_service.register(**invalid_data)
def test_register_with_duplicate_email_raises_error(self, user_service, sample_user_data):
"""Test that duplicate email raises DuplicateEmailError."""
# Arrange
user_service.register(**sample_user_data)
# Act & Assert
with pytest.raises(DuplicateEmailError):
user_service.register(**sample_user_data)
@pytest.mark.parametrize("password,should_fail", [
("short", True), # Too short
("nodigits!", True), # No digits
("NoSpecialChar1", True), # No special char
("Valid1Pass!", False), # Valid
("AnotherGood2@", False), # Valid
])
def test_register_password_validation(self, user_service, password, should_fail):
"""Test password validation with various inputs."""
# Arrange
user_data = {
"email": "test@example.com",
"password": password,
"username": "testuser"
}
# Act & Assert
if should_fail:
with pytest.raises(ValidationError):
user_service.register(**user_data)
else:
user = user_service.register(**user_data)
assert user is not None
class TestUserAuthentication:
"""Tests for user authentication functionality."""
@pytest.fixture
def registered_user(self, user_service, sample_user_data):
"""Provide a registered user for testing."""
return user_service.register(**sample_user_data)
def test_login_with_valid_credentials_returns_token(
self, user_service, registered_user, sample_user_data
):
"""Test that valid credentials return an auth token."""
# Arrange
email = sample_user_data["email"]
password = sample_user_data["password"]
# Act
token = user_service.login(email, password)
# Assert
assert token is not None
assert isinstance(token, str)
assert len(token) > 0
def test_login_with_invalid_password_raises_error(
self, user_service, registered_user, sample_user_data
):
"""Test that invalid password raises AuthenticationError."""
# Arrange
email = sample_user_data["email"]
wrong_password = "WrongPassword123!"
# Act & Assert
with pytest.raises(AuthenticationError):
user_service.login(email, wrong_password)
def test_login_with_nonexistent_user_raises_error(self, user_service):
"""Test that non-existent user raises AuthenticationError."""
# Arrange
email = "nonexistent@example.com"
password = "Password123!"
# Act & Assert
with pytest.raises(AuthenticationError):
user_service.login(email, password)
class TestUserEmailNotification:
"""Tests for user email notification functionality."""
def test_register_sends_welcome_email(self, user_service, sample_user_data, mocker):
"""Test that registration sends welcome email."""
# Arrange
mock_email = mocker.patch("myapp.services.EmailService.send")
# Act
user = user_service.register(**sample_user_data)
# Assert
mock_email.assert_called_once_with(
to=sample_user_data["email"],
subject="Welcome to MyApp!",
template="welcome",
context={"username": user.username}
)
def test_register_does_not_send_email_on_failure(
self, user_service, sample_user_data, mocker
):
"""Test that failed registration does not send email."""
# Arrange
mock_email = mocker.patch("myapp.services.EmailService.send")
invalid_data = {**sample_user_data, "email": "invalid-email"}
# Act
with pytest.raises(ValidationError):
user_service.register(**invalid_data)
# Assert
mock_email.assert_not_called()
```
Example 2: Testing with Fixtures and Factories
```python
"""Example using fixtures and factories for test data."""
import pytest
from myapp.models import User, Post, Comment
@pytest.fixture
def user_factory(db):
"""Provide factory for creating test users."""
created_users = []
def _create_user(**kwargs):
defaults = {
"username": f"user_{len(created_user
More from this repository5
Provides comprehensive Spring Framework and Spring Boot best practices, patterns, and guidelines for enterprise Java application development and code reviews.
Provides comprehensive templates and best practices for creating Claude Code subagents and skills, ensuring consistent and high-quality agent development.
Provides comprehensive FastAPI best practices and patterns for building robust, performant, and well-structured Python web APIs.
Validates Django project structure, naming conventions, and best practices to ensure clean, maintainable, and standardized web application code.
Provides comprehensive, standardized templates and structural guidelines for creating learning repositories across different SDLC methodologies.