npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-pythonWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
This skill should be used when writing Python tests, creating pytest fixtures, configuring conftest.py, parametrizing tests, mocking dependencies, or improving test coverage.
This skill uses the workspace's default tool permissions.
Testing Patterns
This skill defines comprehensive patterns for writing effective, maintainable, and thorough test suites using pytest. These patterns prioritize test clarity, proper isolation, and high coverage while maintaining excellent performance.
Fixtures Over Setup/Teardown
RULE: Always prefer pytest fixtures over traditional setup/teardown methods:
# CORRECT: Using fixtures
from __future__ import annotations
import pytest
from pathlib import Path
from myapp.database import Database
from myapp.models import User
@pytest.fixture
def temp_database(tmp_path: Path) -> Database:
"""Create temporary database for testing."""
db_path = tmp_path / "test.db"
db = Database(db_path)
db.initialize()
return db
@pytest.fixture
def sample_user() -> User:
"""Create sample user for tests."""
return User(user_id=1, username="testuser", email="test@example.com")
def test_user_creation(temp_database: Database, sample_user: User) -> None:
"""Test creating a user in database."""
temp_database.save(sample_user)
retrieved = temp_database.get_user(sample_user.user_id)
assert retrieved == sample_user
# WRONG: Using setup/teardown
import unittest
from myapp.database import Database
class TestDatabase(unittest.TestCase):
def setUp(self):
self.db = Database(":memory:")
self.db.initialize()
def tearDown(self):
self.db.close()
def test_user_creation(self):
user = User(1, "testuser", "test@example.com")
self.db.save(user)
retrieved = self.db.get_user(user.user_id)
self.assertEqual(retrieved, user)
Why fixtures are better:
- Explicit dependencies in function signatures
- Automatic cleanup with yield fixtures
- Composable and reusable across tests
- Support for different scopes
- Better IDE integration and type checking
Fixture Scopes
RULE: Choose appropriate fixture scope based on setup cost and state requirements:
# CORRECT: Different fixture scopes
from __future__ import annotations
import pytest
from collections.abc import Iterator
from pathlib import Path
import httpx
from myapp.app import create_app
from myapp.database import Database
@pytest.fixture(scope="session")
def database_schema() -> str:
"""Load database schema once per test session."""
schema_path = Path(__file__).parent / "schema.sql"
return schema_path.read_text()
@pytest.fixture(scope="module")
def test_database(database_schema: str, tmp_path_factory) -> Iterator[Database]:
"""Create database once per test module."""
db_path = tmp_path_factory.mktemp("data") / "test.db"
db = Database(db_path)
db.execute(database_schema)
yield db
db.close()
@pytest.fixture(scope="function") # Default scope
def clean_database(test_database: Database) -> Iterator[Database]:
"""Provide clean database for each test."""
yield test_database
test_database.clear_all_tables()
@pytest.fixture
def api_client() -> Iterator[httpx.Client]:
"""Create HTTP client for each test."""
with httpx.Client(app=create_app()) as client:
yield client
def test_user_creation(clean_database: Database, api_client: httpx.Client) -> None:
"""Test user creation endpoint."""
response = api_client.post("/users", json={"username": "alice"})
assert response.status_code == 201
user_id = response.json()["user_id"]
user = clean_database.get_user(user_id)
assert user.username == "alice"
Scope guidelines:
function(default): Fast setup, test isolation criticalclass: Shared across test class methodsmodule: Expensive setup, read-only usagesession: Very expensive setup, global resources
Fixture Naming Conventions
RULE: Name fixtures as nouns representing the provided resource:
# CORRECT: Noun-based fixture names
@pytest.fixture
def user() -> User:
"""Sample user for testing."""
return User(user_id=1, username="alice")
@pytest.fixture
def db_session() -> Session:
"""Database session."""
session = Session()
yield session
session.rollback()
session.close()
@pytest.fixture
def auth_client(user: User) -> AuthenticatedClient:
"""HTTP client with authentication."""
return AuthenticatedClient(user=user)
@pytest.fixture
def temp_dir(tmp_path: Path) -> Path:
"""Temporary directory for test files."""
return tmp_path
# WRONG: Verb-based or unclear fixture names
@pytest.fixture
def setup_user(): # Sounds like setup method
return User(user_id=1, username="alice")
@pytest.fixture
def get_db_session(): # Sounds like function
session = Session()
yield session
session.close()
@pytest.fixture
def create_authenticated_client(user): # Too verbose
return AuthenticatedClient(user=user)
conftest.py Hierarchy
RULE: Organize fixtures in conftest.py files following test directory structure:
tests/
├── conftest.py # Global fixtures
├── test_models.py
├── integration/
│ ├── conftest.py # Integration test fixtures
│ ├── test_api.py
│ └── test_database.py
└── unit/
├── conftest.py # Unit test fixtures
├── test_services.py
└── test_utils.py
# tests/conftest.py - Global fixtures
from __future__ import annotations
import pytest
from pathlib import Path
@pytest.fixture(scope="session")
def test_data_dir() -> Path:
"""Directory containing test data files."""
return Path(__file__).parent / "data"
@pytest.fixture
def sample_config() -> dict[str, str]:
"""Sample configuration for tests."""
return {
"api_key": "test-key",
"base_url": "http://localhost:8000",
"timeout": "30",
}
# tests/integration/conftest.py - Integration fixtures
from __future__ import annotations
import pytest
from collections.abc import Iterator
import httpx
from myapp.app import create_app
from myapp.database import Database
@pytest.fixture(scope="module")
def test_database() -> Iterator[Database]:
"""Create test database for integration tests."""
db = Database(":memory:")
db.initialize_schema()
yield db
db.close()
@pytest.fixture
def api_client() -> Iterator[httpx.Client]:
"""HTTP client for API testing."""
app = create_app()
with httpx.Client(app=app, base_url="http://test") as client:
yield client
Test Naming Conventions
RULE: Use descriptive test names following test_<behavior>_<scenario> pattern:
# CORRECT: Descriptive test names
def test_user_creation_with_valid_data() -> None:
"""Test that users are created with valid input."""
user = create_user(username="alice", email="alice@example.com")
assert user.username == "alice"
assert user.email == "alice@example.com"
def test_user_creation_raises_error_on_duplicate_username() -> None:
"""Test that creating user with duplicate username raises error."""
create_user(username="alice", email="alice@example.com")
with pytest.raises(ValueError, match="Username already exists"):
create_user(username="alice", email="different@example.com")
def test_email_validation_rejects_invalid_format() -> None:
"""Test that invalid email format is rejected."""
with pytest.raises(ValueError, match="Invalid email format"):
create_user(username="bob", email="not-an-email")
def test_user_search_returns_empty_list_when_no_matches() -> None:
"""Test that search returns empty list when no users match."""
results = search_users(query="nonexistent")
assert results == []
# WRONG: Poor test names
def test_user(): # What about the user?
user = create_user(username="alice", email="alice@example.com")
assert user.username == "alice"
def test_duplicate(): # Duplicate what?
create_user(username="alice", email="alice@example.com")
with pytest.raises(ValueError):
create_user(username="alice", email="different@example.com")
def test_1(): # Meaningless
results = search_users(query="nonexistent")
assert results == []
Test file naming:
test_<module>.pyfor testing a specific moduletest_integration_<feature>.pyfor integration teststest_e2e_<scenario>.pyfor end-to-end tests
Parametrize for Input Variations
RULE: Use @pytest.mark.parametrize to test multiple input scenarios:
# CORRECT: Parametrized tests with IDs
from __future__ import annotations
import pytest
@pytest.mark.parametrize(
("input_value", "expected"),
[
(0, "zero"),
(1, "one"),
(2, "two"),
(10, "ten"),
],
ids=["zero", "one", "two", "ten"],
)
def test_number_to_word(input_value: int, expected: str) -> None:
"""Test number to word conversion."""
assert number_to_word(input_value) == expected
@pytest.mark.parametrize(
("email", "is_valid"),
[
("user@example.com", True),
("user.name@example.co.uk", True),
("user+tag@example.com", True),
("invalid", False),
("@example.com", False),
("user@", False),
("", False),
],
ids=[
"simple_email",
"subdomain",
"with_plus",
"no_at_sign",
"no_user",
"no_domain",
"empty",
],
)
def test_email_validation(email: str, is_valid: bool) -> None:
"""Test email validation with various formats."""
assert validate_email(email) == is_valid
@pytest.mark.parametrize("user_role", ["admin", "editor", "viewer"])
@pytest.mark.parametrize("resource_type", ["document", "image", "video"])
def test_permission_check(user_role: str, resource_type: str) -> None:
"""Test permission checking for all role/resource combinations."""
user = User(role=user_role)
resource = Resource(type=resource_type)
# Should not raise for any valid combination
check_permission(user, resource, action="read")
# WRONG: Repetitive test functions
def test_number_to_word_zero() -> None:
assert number_to_word(0) == "zero"
def test_number_to_word_one() -> None:
assert number_to_word(1) == "one"
def test_number_to_word_two() -> None:
assert number_to_word(2) == "two"
# WRONG: Testing multiple cases in one test
def test_email_validation() -> None:
assert validate_email("user@example.com") == True
assert validate_email("invalid") == False
assert validate_email("@example.com") == False
# If first assertion fails, others don't run
Parametrize with pytest.param for complex cases:
@pytest.mark.parametrize(
("input_data", "expected_output", "expected_warnings"),
[
pytest.param(
{"name": "Alice", "age": 30},
User(name="Alice", age=30),
[],
id="valid_data",
),
pytest.param(
{"name": "Bob"},
User(name="Bob", age=0),
["Missing age field"],
id="missing_age",
),
pytest.param(
{"name": "", "age": -5},
None,
["Empty name", "Invalid age"],
marks=pytest.mark.xfail(reason="Validation not implemented"),
id="invalid_data",
),
],
)
def test_user_parsing(
input_data: dict[str, int | str],
expected_output: User | None,
expected_warnings: list[str],
) -> None:
"""Test user data parsing with various inputs."""
with warnings.catch_warnings(record=True) as w:
result = parse_user(input_data)
assert result == expected_output
warning_messages = [str(warning.message) for warning in w]
assert warning_messages == expected_warnings
Monkeypatch Over mock.patch
RULE: Prefer monkeypatch fixture over unittest.mock.patch for patching:
# CORRECT: Using monkeypatch
from __future__ import annotations
import pytest
from pathlib import Path
from myapp import config
from myapp.services import external_api
def test_config_loading(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""Test configuration loading from environment."""
config_file = tmp_path / "config.json"
config_file.write_text('{"api_key": "test-key"}')
monkeypatch.setenv("CONFIG_PATH", str(config_file))
loaded_config = config.load()
assert loaded_config["api_key"] == "test-key"
def test_api_call_with_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test API call behavior when timeout occurs."""
def mock_request(*args, **kwargs):
raise TimeoutError("Connection timeout")
monkeypatch.setattr(external_api, "make_request", mock_request)
with pytest.raises(TimeoutError, match="Connection timeout"):
external_api.fetch_data()
def test_current_time_mocking(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test time-dependent behavior with mocked time."""
from datetime import datetime
fixed_time = datetime(2024, 1, 1, 12, 0, 0)
class MockDatetime:
@classmethod
def now(cls):
return fixed_time
monkeypatch.setattr("myapp.utils.datetime", MockDatetime)
result = get_timestamp()
assert result == fixed_time
# WRONG: Using unittest.mock.patch
from unittest.mock import patch
@patch("myapp.config.load")
def test_config_loading(mock_load):
"""Test with patch decorator."""
mock_load.return_value = {"api_key": "test-key"}
# String-based patching is fragile
# Cleanup is less explicit
@patch("myapp.external_api.make_request")
def test_api_call(mock_request):
mock_request.side_effect = TimeoutError("Connection timeout")
# Less clear what's being patched
Why monkeypatch is better:
- Automatic cleanup (restores after test)
- More explicit (see what's being patched)
- Scoped to test function automatically
- Better error messages
- Works with environment variables easily
Factory Pattern for Complex Fixtures
RULE: Use factory fixtures that return callables for flexible test object creation:
# CORRECT: Factory fixture pattern
from __future__ import annotations
import pytest
from collections.abc import Callable
from datetime import datetime
from myapp.models import User, Post
@pytest.fixture
def make_user() -> Callable[..., User]:
"""Factory for creating test users with custom attributes."""
created_users = []
def _make_user(
user_id: int | None = None,
username: str = "testuser",
email: str | None = None,
is_active: bool = True,
**kwargs,
) -> User:
if user_id is None:
user_id = len(created_users) + 1
if email is None:
email = f"{username}@example.com"
user = User(
user_id=user_id,
username=username,
email=email,
is_active=is_active,
**kwargs,
)
created_users.append(user)
return user
return _make_user
@pytest.fixture
def make_post(make_user: Callable[..., User]) -> Callable[..., Post]:
"""Factory for creating test posts."""
def _make_post(
title: str = "Test Post",
content: str = "Test content",
author: User | None = None,
published_at: datetime | None = None,
**kwargs,
) -> Post:
if author is None:
author = make_user()
return Post(
title=title,
content=content,
author=author,
published_at=published_at or datetime.now(),
**kwargs,
)
return _make_post
def test_user_with_defaults(make_user: Callable[..., User]) -> None:
"""Test creating user with default values."""
user = make_user()
assert user.username == "testuser"
assert user.email == "testuser@example.com"
assert user.is_active is True
def test_user_with_custom_values(make_user: Callable[..., User]) -> None:
"""Test creating user with custom values."""
user = make_user(username="alice", email="alice@custom.com", is_active=False)
assert user.username == "alice"
assert user.email == "alice@custom.com"
assert user.is_active is False
def test_multiple_users(make_user: Callable[..., User]) -> None:
"""Test creating multiple users with different attributes."""
alice = make_user(username="alice")
bob = make_user(username="bob")
charlie = make_user(username="charlie", is_active=False)
assert alice.user_id != bob.user_id != charlie.user_id
assert charlie.is_active is False
def test_post_creation(make_post: Callable[..., Post]) -> None:
"""Test creating post with auto-generated author."""
post = make_post(title="My Post")
assert post.title == "My Post"
assert post.author.username == "testuser"
# WRONG: Hard-coded fixtures without flexibility
@pytest.fixture
def user() -> User:
"""Fixed user fixture - not flexible."""
return User(user_id=1, username="testuser", email="test@example.com")
def test_multiple_users(user: User) -> None:
"""Can't easily create multiple different users."""
# Have to manually create variations
alice = User(user_id=2, username="alice", email="alice@example.com")
bob = User(user_id=3, username="bob", email="bob@example.com")
Assertion Patterns
RULE: Use clear, descriptive assertions with appropriate methods:
# CORRECT: Clear assertions with messages
from __future__ import annotations
import pytest
def test_user_creation() -> None:
"""Test user creation with proper assertions."""
user = create_user(username="alice", email="alice@example.com")
# Use direct assertions
assert user.username == "alice"
assert user.email == "alice@example.com"
assert user.is_active is True
# Check types when important
assert isinstance(user.user_id, int)
assert user.user_id > 0
def test_collection_operations() -> None:
"""Test collection assertions."""
users = get_all_users()
# Check collection properties
assert len(users) == 3
assert all(user.is_active for user in users)
# Check membership
usernames = [user.username for user in users]
assert "alice" in usernames
assert "banned_user" not in usernames
def test_float_comparison() -> None:
"""Test floating point comparison."""
result = calculate_average([1.0, 2.0, 3.0])
# Use pytest.approx for floats
assert result == pytest.approx(2.0)
assert result == pytest.approx(2.0, abs=0.01)
def test_exception_with_message() -> None:
"""Test exception is raised with specific message."""
with pytest.raises(ValueError, match="Username must be alphanumeric"):
create_user(username="alice!", email="alice@example.com")
def test_multiple_exceptions() -> None:
"""Test multiple exception scenarios."""
with pytest.raises(ValueError, match="Username too short"):
create_user(username="a", email="alice@example.com")
with pytest.raises(ValueError, match="Invalid email"):
create_user(username="alice", email="not-an-email")
# WRONG: Poor assertion patterns
def test_user_creation() -> None:
user = create_user(username="alice", email="alice@example.com")
# Asserting on repr/str is fragile
assert str(user) == "User(alice, alice@example.com)" # Breaks if repr changes
# No descriptive message
assert user.is_active # What does True mean here?
def test_float_comparison() -> None:
result = calculate_average([1.0, 2.0, 3.0])
# Direct float comparison is unreliable
assert result == 2.0 # May fail due to precision
def test_exception_without_match() -> None:
# Too broad - any ValueError passes
with pytest.raises(ValueError):
create_user(username="alice!", email="alice@example.com")
Test Markers
RULE: Use markers to categorize and selectively run tests:
# CORRECT: Using test markers
from __future__ import annotations
import pytest
@pytest.mark.unit
def test_user_validation() -> None:
"""Fast unit test for user validation."""
assert validate_username("alice") is True
@pytest.mark.integration
def test_database_user_creation(db_session) -> None:
"""Integration test with database."""
user = User(username="alice")
db_session.add(user)
db_session.commit()
retrieved = db_session.query(User).filter_by(username="alice").first()
assert retrieved.username == "alice"
@pytest.mark.slow
@pytest.mark.integration
def test_bulk_data_import(db_session) -> None:
"""Slow integration test for bulk operations."""
users = [User(username=f"user{i}") for i in range(10000)]
db_session.bulk_save_objects(users)
db_session.commit()
count = db_session.query(User).count()
assert count == 10000
@pytest.mark.external
def test_api_call() -> None:
"""Test requiring external API access."""
response = call_external_api()
assert response.status_code == 200
@pytest.mark.parametrize("value", [1, 2, 3])
@pytest.mark.skip(reason="Feature not implemented yet")
def test_future_feature(value: int) -> None:
"""Test for upcoming feature."""
assert process_value(value) > 0
@pytest.mark.xfail(reason="Known bug in dependency")
def test_with_known_bug() -> None:
"""Test that currently fails due to known issue."""
assert buggy_function() == expected_value
Configure markers in pyproject.toml:
[tool.pytest.ini_options]
markers = [
"unit: Fast unit tests",
"integration: Integration tests with external dependencies",
"slow: Slow-running tests",
"external: Tests requiring external services",
]
Run specific marker groups:
# Run only unit tests
uv run pytest -m unit
# Run everything except slow tests
uv run pytest -m "not slow"
# Run integration tests excluding external dependencies
uv run pytest -m "integration and not external"
Async Test Patterns
RULE: Use pytest-asyncio for testing async code:
# CORRECT: Async test patterns
from __future__ import annotations
import pytest
import pytest_asyncio
from collections.abc import AsyncIterator
import httpx
from myapp.services import AsyncUserService
# Configure pytest-asyncio in pyproject.toml:
# [tool.pytest.ini_options]
# asyncio_mode = "auto"
@pytest_asyncio.fixture
async def async_client() -> AsyncIterator[httpx.AsyncClient]:
"""Async HTTP client fixture."""
async with httpx.AsyncClient(base_url="http://test") as client:
yield client
@pytest_asyncio.fixture
async def user_service() -> AsyncUserService:
"""Async user service fixture."""
service = AsyncUserService()
await service.initialize()
return service
@pytest.mark.asyncio
async def test_async_user_creation(user_service: AsyncUserService) -> None:
"""Test async user creation."""
user = await user_service.create_user(username="alice")
assert user.username == "alice"
retrieved = await user_service.get_user(user.user_id)
assert retrieved == user
@pytest.mark.asyncio
async def test_concurrent_operations(user_service: AsyncUserService) -> None:
"""Test concurrent async operations."""
import asyncio
# Create multiple users concurrently
tasks = [
user_service.create_user(username=f"user{i}")
for i in range(10)
]
users = await asyncio.gather(*tasks)
assert len(users) == 10
assert all(user.username.startswith("user") for user in users)
@pytest.mark.asyncio
async def test_async_context_manager(async_client: httpx.AsyncClient) -> None:
"""Test with async context manager."""
response = await async_client.get("/users")
assert response.status_code == 200
users = response.json()
assert isinstance(users, list)
# WRONG: Mixing sync and async incorrectly
def test_async_user_creation(user_service): # Missing async
user = await user_service.create_user(username="alice") # SyntaxError
assert user.username == "alice"
@pytest.mark.asyncio
async def test_with_sync_fixture(sync_client): # Fixture should be async
response = sync_client.get("/users") # Blocking call in async test
assert response.status_code == 200
Coverage Best Practices
RULE: Aim for high coverage with meaningful tests:
# CORRECT: Comprehensive test coverage
from __future__ import annotations
import pytest
from typing import TYPE_CHECKING
from myapp.calculator import Calculator
def test_calculator_addition() -> None:
"""Test addition operation."""
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
assert calc.add(0, 0) == 0
def test_calculator_division() -> None:
"""Test division operation."""
calc = Calculator()
assert calc.divide(6, 2) == 3
assert calc.divide(5, 2) == 2.5
def test_calculator_division_by_zero() -> None:
"""Test division by zero raises error."""
calc = Calculator()
with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"):
calc.divide(5, 0)
def test_calculator_operation_history() -> None:
"""Test operation history tracking."""
calc = Calculator()
calc.add(2, 3)
calc.divide(10, 2)
history = calc.get_history()
assert len(history) == 2
assert history[0] == "add(2, 3) = 5"
assert history[1] == "divide(10, 2) = 5.0"
# Legitimate use of pragma: no cover
if TYPE_CHECKING: # pragma: no cover
from myapp.models import User
# Defensive code that can't be tested
def safe_divide(a: float, b: float) -> float:
"""Safely divide two numbers."""
try:
return a / b
except Exception as e: # pragma: no cover
# This should never happen, but defensive
logger.critical("Unexpected error in safe_divide: %s", e)
raise
Configure coverage in pyproject.toml:
[tool.coverage.run]
source = ["src"]
branch = true
omit = [
"*/tests/*",
"*/migrations/*",
"*/__main__.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
"@abstractmethod",
]
show_missing = true
fail_under = 90
Run coverage:
# Run tests with coverage
uv run pytest --cov=src --cov-report=html --cov-report=term
# Check coverage threshold
uv run pytest --cov=src --cov-fail-under=90
Testing Best Practices Summary
Test Structure
AAA Pattern: Arrange, Act, Assert
def test_user_creation() -> None:
"""Test user creation with valid data."""
# Arrange
username = "alice"
email = "alice@example.com"
# Act
user = create_user(username=username, email=email)
# Assert
assert user.username == username
assert user.email == email
Test Independence
RULE: Each test must be independent and isolated:
# CORRECT: Independent tests
@pytest.fixture
def clean_database(database):
"""Provide clean database for each test."""
yield database
database.clear()
def test_user_creation(clean_database) -> None:
"""Test creates its own data."""
user = User(username="alice")
clean_database.save(user)
assert clean_database.count() == 1
def test_user_deletion(clean_database) -> None:
"""Test doesn't depend on previous test."""
user = User(username="bob")
clean_database.save(user)
clean_database.delete(user)
assert clean_database.count() == 0
# WRONG: Tests depend on each other
def test_user_creation() -> None:
"""Creates user that other tests depend on."""
global created_user
created_user = User(username="alice")
database.save(created_user)
def test_user_deletion() -> None:
"""Depends on test_user_creation running first."""
database.delete(created_user) # Fails if test_user_creation didn't run
Performance Considerations
# CORRECT: Efficient test setup
@pytest.fixture(scope="module")
def expensive_resource():
"""Setup expensive resource once per module."""
resource = ExpensiveResource()
resource.initialize() # Slow operation
yield resource
resource.cleanup()
@pytest.fixture
def clean_resource(expensive_resource):
"""Reset resource state for each test."""
yield expensive_resource
expensive_resource.reset() # Fast operation
Testing Private Methods
RULE: Test behavior through public interface, not private methods:
# CORRECT: Test public interface
def test_user_password_validation() -> None:
"""Test password validation through public method."""
user = User(username="alice")
# Test through public method
user.set_password("weakpw")
assert user.is_password_valid("weakpw") is True
assert user.is_password_valid("wrongpw") is False
# WRONG: Testing private methods directly
def test_user_password_hashing() -> None:
"""Testing implementation detail."""
user = User(username="alice")
# Testing private method
hashed = user._hash_password("password") # Implementation detail
assert len(hashed) == 64
Anti-Patterns to Avoid
1. Testing Implementation Instead of Behavior
# WRONG: Testing implementation
def test_user_service_calls_repository() -> None:
"""Test that service calls repository method."""
repo = Mock()
service = UserService(repo)
service.get_user(1)
repo.find_by_id.assert_called_once_with(1) # Testing implementation
# CORRECT: Testing behavior
def test_user_service_returns_user() -> None:
"""Test that service returns correct user."""
repo = InMemoryUserRepository()
repo.save(User(user_id=1, username="alice"))
service = UserService(repo)
user = service.get_user(1)
assert user.username == "alice" # Testing behavior
2. Overly Complex Test Setup
# WRONG: Complex test with too much setup
def test_order_processing() -> None:
db = Database()
db.connect()
db.create_tables()
user = User(username="alice")
db.save(user)
product1 = Product(name="Widget")
db.save(product1)
product2 = Product(name="Gadget")
db.save(product2)
cart = ShoppingCart(user=user)
cart.add_item(product1, quantity=2)
cart.add_item(product2, quantity=1)
order = process_order(cart)
assert order.total == 30
# CORRECT: Use fixtures to simplify
def test_order_processing(user, cart_with_items) -> None:
"""Test order processing with fixtures."""
order = process_order(cart_with_items)
assert order.total == 30
3. Not Using Specific Exception Matching
# WRONG: Too broad exception testing
def test_invalid_email() -> None:
with pytest.raises(ValueError): # Any ValueError passes
validate_email("invalid")
# CORRECT: Specific exception message
def test_invalid_email() -> None:
with pytest.raises(ValueError, match="Invalid email format"):
validate_email("invalid")
Summary Checklist
When writing tests, ensure:
- Using fixtures instead of setup/teardown
- Fixture names are nouns (not verbs)
- Appropriate fixture scope chosen
- Tests named as
test_<behavior>_<scenario> - Using
@pytest.mark.parametrizefor input variations - Providing
idsfor parametrized tests - Using
monkeypatchinstead ofmock.patch - Factory fixtures for complex object creation
- Specific
matchparameter onpytest.raises - Using
pytest.approxfor float comparisons - Never asserting on object repr/str
- Markers registered in pyproject.toml
- Async tests use
@pytest.mark.asyncio - Coverage target >= 90%
- Tests are independent and isolated
- Testing behavior, not implementation
These patterns ensure test suites are maintainable, reliable, and provide confidence in code quality.
Similar Skills
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.