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.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add mattnigh/skills_collection --skill collection --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Collection?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mattnigh-collection-aafeb1a1)More formats (shields.io, HTML) on the badges page.
---
name: python-testing-standards
description: 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.
allowed-tools: Read, Write, Grep, Glob
---
# 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_<what>_<when>_<expected_behavior>`
✅ **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:**
1. **Arrange**: Set up test data and preconditions
2. **Act**: Execute the behavior being tested
3. **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:**
1. **Given**: Preconditions and setup (same as Arrange)
2. **When**: Action being tested (same as Act)
3. **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 class
- `module`: Runs once per module
- `session`: 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 `function` scope for fixtures that need to be fresh for each test
- Use `module` or `session` scope 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_<what>_<when>_<expected>`
✅ **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_users)}",
"email": f"user{len(created_users)}@example.com",
"is_active": True
}
defaults.update(kwargs)
user = User(**defaults)
db.save(user)
created_users.append(user)
return user
yield _create_user
# Cleanup
for user in created_users:
db.delete(user)
@pytest.fixture
def post_factory(db, user_factory):
"""Provide factory for creating test posts."""
created_posts = []
def _create_post(**kwargs):
if "author" not in kwargs:
kwargs["author"] = user_factory()
defaults = {
"title": f"Post {len(created_posts)}",
"content": "Test content"
}
defaults.update(kwargs)
post = Post(**defaults)
db.save(post)
created_posts.append(post)
return post
yield _create_post
# Cleanup
for post in created_posts:
db.delete(post)
def test_user_can_create_multiple_posts(user_factory, post_factory):
"""Test that user can create multiple posts."""
# Arrange
user = user_factory(username="author")
# Act
post1 = post_factory(author=user, title="First Post")
post2 = post_factory(author=user, title="Second Post")
# Assert
assert post1.author == user
assert post2.author == user
assert user.posts.count() == 2
def test_post_without_author_creates_default_user(post_factory):
"""Test that post without author gets default user."""
# Act
post = post_factory(title="Test Post")
# Assert
assert post.author is not None
assert post.author.username.startswith("user_")
```
### Example 3: Integration Test with Database
```python
"""Integration tests with real database."""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from myapp.models import Base, User
from myapp.repositories import UserRepository
@pytest.fixture(scope="module")
def engine():
"""Create in-memory SQLite engine for testing."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine):
"""Provide database session with transaction rollback."""
connection = engine.connect()
transaction = connection.begin()
Session = sessionmaker(bind=connection)
session = Session()
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def user_repository(db_session):
"""Provide user repository instance."""
return UserRepository(session=db_session)
def test_create_user_persists_to_database(user_repository):
"""Test that created user is persisted to database."""
# Arrange
user_data = {
"username": "testuser",
"email": "test@example.com"
}
# Act
user = user_repository.create(**user_data)
retrieved_user = user_repository.find_by_id(user.id)
# Assert
assert retrieved_user is not None
assert retrieved_user.username == "testuser"
assert retrieved_user.email == "test@example.com"
def test_find_by_email_returns_correct_user(user_repository):
"""Test finding user by email returns correct user."""
# Arrange
user1 = user_repository.create(username="user1", email="user1@example.com")
user2 = user_repository.create(username="user2", email="user2@example.com")
# Act
found_user = user_repository.find_by_email("user2@example.com")
# Assert
assert found_user is not None
assert found_user.id == user2.id
assert found_user.username == "user2"
```
---
## Templates
### Template 1: Basic Unit Test
Located at: `templates/unit_test_template.py`
**Purpose:** Template for basic unit tests using AAA pattern.
**Usage:**
1. Copy template to `tests/unit/test_[module_name].py`
2. Replace `[MODULE_NAME]` with actual module name
3. Replace `[function/class]` with code under test
4. Add test cases following the pattern
See: `/Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/python-testing-standards/templates/unit_test_template.py`
### Template 2: Integration Test with Database
Located at: `templates/integration_test_template.py`
**Purpose:** Template for integration tests with database fixtures.
**Usage:**
1. Copy template to `tests/integration/test_[feature_name].py`
2. Configure database engine for your database
3. Add test cases using `db_session` fixture
See: `/Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/python-testing-standards/templates/integration_test_template.py`
### Template 3: Test with Mocking
Located at: `templates/mock_test_template.py`
**Purpose:** Template for tests using mocks and patches.
**Usage:**
1. Copy template to appropriate test directory
2. Replace mock targets with actual dependencies
3. Add verification assertions
See: `/Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/python-testing-standards/templates/mock_test_template.py`
---
## Decision Trees
### When to Mock vs Test Real Implementation
```
Is the dependency external (API, database, file system)?
├─ Yes → Mock it
│ └─ Example: mock requests.get(), EmailService.send()
└─ No → Is it slow (>100ms)?
├─ Yes → Mock it
│ └─ Example: mock expensive computations, large file processing
└─ No → Is it non-deterministic (random, time)?
├─ Yes → Mock it
│ └─ Example: mock datetime.now(), random.randint()
└─ No → Is it your internal business logic?
├─ Yes → Test it directly (don't mock)
│ └─ Example: test calculation functions, validators
└─ No → Is it a framework/library?
├─ Yes → Don't test it (trust the framework)
└─ No → Consider mocking if needed for isolation
```
### Choosing Fixture Scope
```
Does the fixture need to be fresh for each test?
├─ Yes → Use scope="function" (default)
│ └─ Example: test data, mutable objects
└─ No → Is the fixture expensive to create?
├─ Yes → Is it stateless/read-only?
│ ├─ Yes → Use scope="module" or "session"
│ │ └─ Example: database connection, API client
│ └─ No → Use scope="function" with cleanup
│ └─ Example: database with data
└─ No → Use scope="function" for simplicity
```
### Test Organization Strategy
```
What are you testing?
├─ Single function/method with no dependencies
│ └─ Use standalone test function
│ └─ Example: def test_calculate_sum()
├─ Multiple related functions/methods
│ └─ Use test class for grouping
│ └─ Example: class TestUserAuthentication
├─ Tests sharing setup/teardown
│ └─ Use test class with fixtures
│ └─ Example: class TestDatabaseOperations with setup fixtures
└─ Different test types (unit/integration/e2e)
└─ Use separate directories
└─ Example: tests/unit/, tests/integration/, tests/e2e/
```
---
## Common Pitfalls
### Pitfall 1: Mocking Too Much
**Problem:** Over-mocking makes tests brittle and defeats the purpose of testing.
**Why it happens:** Desire for fast tests or lack of understanding of what to mock.
**How to avoid:**
- Only mock external dependencies and slow operations
- Test real implementation of your business logic
- Use integration tests for testing components together
**Example:**
❌ **Bad:**
```python
def test_user_service_register(mocker):
"""Over-mocked test that doesn't test anything real."""
mock_user = mocker.Mock()
mock_validator = mocker.patch("myapp.validators.EmailValidator")
mock_hasher = mocker.patch("myapp.security.hash_password")
mock_repo = mocker.patch("myapp.repositories.UserRepository")
# This test doesn't test any real code!
service = UserService()
service.register("test@example.com", "password")
mock_validator.validate.assert_called_once()
mock_hasher.assert_called_once()
mock_repo.save.assert_called_once()
```
✅ **Good:**
```python
def test_user_service_register(mocker):
"""Test real logic, mock only external dependencies."""
# Only mock external dependencies (database, email)
mock_email = mocker.patch("myapp.services.EmailService.send")
# Test real validation, hashing, and service logic
service = UserService()
user = service.register("test@example.com", "SecurePass123!")
# Verify real behavior
assert user.email == "test@example.com"
assert user.password_hash != "SecurePass123!" # Password was hashed
mock_email.assert_called_once() # Email was sent
```
### Pitfall 2: Testing Private Methods
**Problem:** Tests are coupled to implementation details and break on refactoring.
**Why it happens:** Misconception that 100% coverage requires testing private methods.
**How to avoid:**
- Test public API/interface only
- Private methods are tested indirectly through public methods
- If a private method seems complex enough to test directly, it might deserve to be a separate class
**Example:**
❌ **Bad:**
```python
class UserService:
def register(self, email, password):
self._validate_email(email)
self._validate_password(password)
return self._create_user(email, password)
def _validate_email(self, email):
# Private validation logic
pass
def _create_user(self, email, password):
# Private creation logic
pass
# DON'T DO THIS
def test_validate_email():
"""Testing private method directly."""
service = UserService()
service._validate_email("test@example.com") # Bad!
```
✅ **Good:**
```python
# Test the public API
def test_register_with_invalid_email_raises_error():
"""Test registration validates email (tests _validate_email indirectly)."""
service = UserService()
with pytest.raises(ValidationError):
service.register("invalid-email", "SecurePass123!")
def test_register_with_valid_data_creates_user():
"""Test registration creates user (tests _create_user indirectly)."""
service = UserService()
user = service.register("test@example.com", "SecurePass123!")
assert user.email == "test@example.com"
```
### Pitfall 3: Shared State Between Tests
**Problem:** Tests fail or pass depending on execution order.
**Why it happens:** Using module-level or class-level mutable state without proper cleanup.
**How to avoid:**
- Use fixtures with proper scope
- Clean up state in fixtures (use yield for teardown)
- Avoid module-level globals
- Use `autouse=True` fixtures for necessary cleanup
**Example:**
❌ **Bad:**
```python
# Module-level shared state
_test_users = []
def test_create_user():
"""Test depends on _test_users being empty."""
user = User(username="test")
_test_users.append(user)
assert len(_test_users) == 1 # Fails if other tests ran first!
def test_delete_user():
"""Test depends on test_create_user."""
_test_users.pop()
assert len(_test_users) == 0
```
✅ **Good:**
```python
@pytest.fixture
def user_list():
"""Provide fresh list for each test."""
users = []
yield users
users.clear() # Cleanup (though not needed with function scope)
def test_create_user(user_list):
"""Test with isolated state."""
user = User(username="test")
user_list.append(user)
assert len(user_list) == 1
def test_delete_user(user_list):
"""Test with isolated state."""
user = User(username="test")
user_list.append(user)
user_list.pop()
assert len(user_list) == 0
```
### Pitfall 4: Not Cleaning Up Resources
**Problem:** Tests leave behind files, database records, or open connections.
**Why it happens:** Forgetting to add cleanup code or not using fixtures properly.
**How to avoid:**
- Use fixtures with yield for setup/teardown
- Use context managers
- Use temp directories for file tests
- Use transaction rollback for database tests
**Example:**
❌ **Bad:**
```python
def test_file_processing():
"""Test leaves file behind."""
with open("test_file.txt", "w") as f:
f.write("test data")
result = process_file("test_file.txt")
assert result == "processed"
# File left behind!
```
✅ **Good:**
```python
import tempfile
import os
@pytest.fixture
def test_file():
"""Provide temporary test file."""
fd, path = tempfile.mkstemp(suffix=".txt")
# Write test data
with os.fdopen(fd, "w") as f:
f.write("test data")
yield path
# Cleanup
if os.path.exists(path):
os.remove(path)
def test_file_processing(test_file):
"""Test with automatic cleanup."""
result = process_file(test_file)
assert result == "processed"
# File automatically cleaned up
```
### Pitfall 5: Unclear Test Failure Messages
**Problem:** When test fails, it's not clear what went wrong.
**Why it happens:** Using bare assertions without context or descriptive messages.
**How to avoid:**
- Use descriptive test names
- Add assertion messages for complex checks
- Use pytest's assertion rewriting (automatic for simple assertions)
- Use `pytest.raises()` with `match` parameter
**Example:**
❌ **Bad:**
```python
def test_user():
"""Vague test name."""
u = User("test@example.com")
assert u.email == "test@example.org" # Failure message unclear
```
When this fails:
```
assert 'test@example.com' == 'test@example.org'
```
✅ **Good:**
```python
def test_user_email_is_stored_correctly():
"""Descriptive test name explains what's being tested."""
# Arrange
email = "test@example.com"
# Act
user = User(email)
# Assert
assert user.email == email, f"Expected user email to be {email}, got {user.email}"
```
When this fails:
```
AssertionError: Expected user email to be test@example.com, got test@example.org
```
**For Exceptions:**
✅ **Good:**
```python
def test_invalid_email_raises_validation_error():
"""Test that invalid email raises ValidationError with helpful message."""
with pytest.raises(ValidationError, match="Invalid email format"):
User("not-an-email")
```
---
## Checklist
Use this checklist during code reviews to verify test quality:
### Test File Organization
- [ ] Test files named `test_*.py` or `*_test.py`
- [ ] Tests organized in logical directories (`unit/`, `integration/`, `e2e/`)
- [ ] Shared fixtures in `conftest.py` at appropriate levels
- [ ] One test file per module/class being tested
### Test Function/Class Naming
- [ ] Test functions start with `test_`
- [ ] Test classes start with `Test`
- [ ] Names describe the scenario: `test_<what>_<when>_<expected>`
- [ ] Names are clear and self-documenting
### Test Structure
- [ ] Tests follow AAA or Given-When-Then pattern
- [ ] Clear separation between Arrange, Act, Assert
- [ ] Each test verifies one logical behavior
- [ ] Tests are independent and can run in any order
### Fixtures
- [ ] Appropriate fixture scope selected (function/class/module/session)
- [ ] Fixtures have descriptive names
- [ ] Fixtures clean up resources (using yield)
- [ ] Fixture dependencies are logical and clear
- [ ] `autouse` only used when necessary
### Mocking and Patching
- [ ] Only external dependencies are mocked
- [ ] Internal business logic is tested directly
- [ ] Patches target where object is used, not where it's defined
- [ ] Mock assertions verify expected behavior
- [ ] Mocks are reset between tests (automatic with mocker fixture)
### Test Coverage
- [ ] Critical paths have 100% coverage
- [ ] Overall coverage meets project threshold (80%+)
- [ ] Coverage report excludes irrelevant files (migrations, etc.)
- [ ] Business logic is thoroughly tested
- [ ] Edge cases are covered
### Assertions
- [ ] Tests have at least one assertion
- [ ] Assertions are specific and meaningful
- [ ] Exception tests use `pytest.raises()`
- [ ] Assertion messages provided for complex checks
- [ ] No bare `assert` without verification
### Test Quality
- [ ] No testing of implementation details
- [ ] No overly complex test setup
- [ ] Tests are not flaky (deterministic)
- [ ] Edge cases are tested
- [ ] Negative test cases are included
- [ ] Tests are fast (unit tests < 100ms)
### Parametrization
- [ ] `@pytest.mark.parametrize` used for multiple similar test cases
- [ ] Test IDs provided for clarity (using `pytest.param`)
- [ ] Parametrized tests cover full range of inputs
### Code Quality
- [ ] Tests follow PEP 8 style guide
- [ ] Tests have docstrings describing what they test
- [ ] No code duplication (use fixtures/helpers)
- [ ] Imports are organized and minimal
- [ ] No commented-out code
### Documentation
- [ ] README explains how to run tests
- [ ] Complex test scenarios are documented
- [ ] Custom markers are registered and documented
- [ ] Fixture purposes are clear from docstrings
---
## Tools and Commands
### Running Tests
```bash
# Run all tests
pytest
# Run specific test file
pytest tests/unit/test_user_service.py
# Run specific test function
pytest tests/unit/test_user_service.py::test_register_with_valid_data
# Run specific test class
pytest tests/unit/test_user_service.py::TestUserRegistration
# Run tests matching pattern
pytest -k "registration"
# Run tests with specific marker
pytest -m "slow"
# Run tests excluding marker
pytest -m "not slow"
# Verbose output
pytest -v
# Show print statements
pytest -s
# Stop after first failure
pytest -x
# Run last failed tests only
pytest --lf
# Run failed tests first, then rest
pytest --ff
# Show slowest tests
pytest --durations=10
```
### Coverage Commands
```bash
# Run with coverage
pytest --cov=myapp
# HTML report
pytest --cov=myapp --cov-report=html
# Terminal report with missing lines
pytest --cov=myapp --cov-report=term-missing
# XML report (for CI)
pytest --cov=myapp --cov-report=xml
# Fail if coverage below threshold
pytest --cov=myapp --cov-fail-under=80
# Multiple reports
pytest --cov=myapp --cov-report=html --cov-report=term --cov-report=xml
```
### Pytest Configuration
**pytest.ini:**
```ini
[pytest]
# Test discovery patterns
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Default options
addopts =
-v
--strict-markers
--tb=short
--cov=myapp
--cov-report=term-missing
--cov-fail-under=80
# Test paths
testpaths = tests
# Markers
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
e2e: marks tests as end-to-end tests
# Coverage options
[coverage:run]
source = myapp
omit =
*/tests/*
*/migrations/*
*/venv/*
*/__pycache__/*
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
```
### Useful Pytest Plugins
```bash
# Install common plugins
pip install pytest-cov # Coverage integration
pip install pytest-mock # Mocking integration
pip install pytest-xdist # Parallel test execution
pip install pytest-django # Django integration
pip install pytest-asyncio # Async test support
pip install pytest-timeout # Test timeouts
pip install pytest-benchmark # Performance benchmarking
# Run tests in parallel
pytest -n auto # Uses all CPU cores
pytest -n 4 # Uses 4 workers
# Set test timeout
pytest --timeout=10 # 10 seconds per test
```
---
## Related Skills
- **uncle-duke-python**: Python code review agent that uses this skill as reference
- **agent-skill-templates**: Templates for creating new skills
---
## References
### Official Documentation
- [pytest Documentation](https://docs.pytest.org/)
- [unittest.mock Documentation](https://docs.python.org/3/library/unittest.mock.html)
- [pytest-cov Documentation](https://pytest-cov.readthedocs.io/)
- [pytest-mock Documentation](https://pytest-mock.readthedocs.io/)
### Best Practices Guides
- [Effective Python Testing with pytest (Book)](https://pragprog.com/titles/bopytest/python-testing-with-pytest/)
- [Python Testing Best Practices](https://docs.python-guide.org/writing/tests/)
- [AAA Pattern](https://wiki.c2.com/?ArrangeActAssert)
- [Given-When-Then](https://martinfowler.com/bliki/GivenWhenThen.html)
### PEPs Referenced
- [PEP 8 - Style Guide for Python Code](https://peps.python.org/pep-0008/)
- [PEP 20 - The Zen of Python](https://peps.python.org/pep-0020/)
- [PEP 484 - Type Hints](https://peps.python.org/pep-0484/)
---
**Version:** 1.0
**Last Updated:** 2025-12-24
**Maintainer:** Development Team
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!