python-testing-patterns
Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
Best use case
python-testing-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
Teams using python-testing-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/python-testing-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How python-testing-patterns Compares
| Feature / Agent | python-testing-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# Python Testing Patterns
Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.
## When to Use This Skill
- Writing unit tests for Python code
- Setting up test suites and test infrastructure
- Implementing test-driven development (TDD)
- Creating integration tests for APIs and services
- Mocking external dependencies and services
- Testing async code and concurrent operations
- Setting up continuous testing in CI/CD
- Implementing property-based testing
- Testing database operations
- Debugging failing tests
## Core Concepts
### 1. Test Types
- **Unit Tests**: Test individual functions/classes in isolation
- **Integration Tests**: Test interaction between components
- **Functional Tests**: Test complete features end-to-end
- **Performance Tests**: Measure speed and resource usage
### 2. Test Structure (AAA Pattern)
- **Arrange**: Set up test data and preconditions
- **Act**: Execute the code under test
- **Assert**: Verify the results
### 3. Test Coverage
- Measure what code is exercised by tests
- Identify untested code paths
- Aim for meaningful coverage, not just high percentages
### 4. Test Isolation
- Tests should be independent
- No shared state between tests
- Each test should clean up after itself
## Quick Start
```python
# test_example.py
def add(a, b):
return a + b
def test_add():
"""Basic test example."""
result = add(2, 3)
assert result == 5
def test_add_negative():
"""Test with negative numbers."""
assert add(-1, 1) == 0
# Run with: pytest test_example.py
```
## Fundamental Patterns
### Pattern 1: Basic pytest Tests
```python
# test_calculator.py
import pytest
class Calculator:
"""Simple calculator for testing."""
def add(self, a: float, b: float) -> float:
return a + b
def subtract(self, a: float, b: float) -> float:
return a - b
def multiply(self, a: float, b: float) -> float:
return a * b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_addition():
"""Test addition."""
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
assert calc.add(0, 0) == 0
def test_subtraction():
"""Test subtraction."""
calc = Calculator()
assert calc.subtract(5, 3) == 2
assert calc.subtract(0, 5) == -5
def test_multiplication():
"""Test multiplication."""
calc = Calculator()
assert calc.multiply(3, 4) == 12
assert calc.multiply(0, 5) == 0
def test_division():
"""Test division."""
calc = Calculator()
assert calc.divide(6, 3) == 2
assert calc.divide(5, 2) == 2.5
def test_division_by_zero():
"""Test division by zero raises error."""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(5, 0)
```
### Pattern 2: Fixtures for Setup and Teardown
```python
# test_database.py
import pytest
from typing import Generator
class Database:
"""Simple database class."""
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.connected = False
def connect(self):
"""Connect to database."""
self.connected = True
def disconnect(self):
"""Disconnect from database."""
self.connected = False
def query(self, sql: str) -> list:
"""Execute query."""
if not self.connected:
raise RuntimeError("Not connected")
return [{"id": 1, "name": "Test"}]
@pytest.fixture
def db() -> Generator[Database, None, None]:
"""Fixture that provides connected database."""
# Setup
database = Database("sqlite:///:memory:")
database.connect()
# Provide to test
yield database
# Teardown
database.disconnect()
def test_database_query(db):
"""Test database query with fixture."""
results = db.query("SELECT * FROM users")
assert len(results) == 1
assert results[0]["name"] == "Test"
@pytest.fixture(scope="session")
def app_config():
"""Session-scoped fixture - created once per test session."""
return {
"database_url": "postgresql://localhost/test",
"api_key": "test-key",
"debug": True
}
@pytest.fixture(scope="module")
def api_client(app_config):
"""Module-scoped fixture - created once per test module."""
# Setup expensive resource
client = {"config": app_config, "session": "active"}
yield client
# Cleanup
client["session"] = "closed"
def test_api_client(api_client):
"""Test using api client fixture."""
assert api_client["session"] == "active"
assert api_client["config"]["debug"] is True
```
### Pattern 3: Parameterized Tests
```python
# test_validation.py
import pytest
def is_valid_email(email: str) -> bool:
"""Check if email is valid."""
return "@" in email and "." in email.split("@")[1]
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("test.user@domain.co.uk", True),
("invalid.email", False),
("@example.com", False),
("user@domain", False),
("", False),
])
def test_email_validation(email, expected):
"""Test email validation with various inputs."""
assert is_valid_email(email) == expected
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
(-5, -5, -10),
])
def test_addition_parameterized(a, b, expected):
"""Test addition with multiple parameter sets."""
from test_calculator import Calculator
calc = Calculator()
assert calc.add(a, b) == expected
# Using pytest.param for special cases
@pytest.mark.parametrize("value,expected", [
pytest.param(1, True, id="positive"),
pytest.param(0, False, id="zero"),
pytest.param(-1, False, id="negative"),
])
def test_is_positive(value, expected):
"""Test with custom test IDs."""
assert (value > 0) == expected
```
### Pattern 4: Mocking with unittest.mock
```python
# test_api_client.py
import pytest
from unittest.mock import Mock, patch, MagicMock
import requests
class APIClient:
"""Simple API client."""
def __init__(self, base_url: str):
self.base_url = base_url
def get_user(self, user_id: int) -> dict:
"""Fetch user from API."""
response = requests.get(f"{self.base_url}/users/{user_id}")
response.raise_for_status()
return response.json()
def create_user(self, data: dict) -> dict:
"""Create new user."""
response = requests.post(f"{self.base_url}/users", json=data)
response.raise_for_status()
return response.json()
def test_get_user_success():
"""Test successful API call with mock."""
client = APIClient("https://api.example.com")
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
mock_response.raise_for_status.return_value = None
with patch("requests.get", return_value=mock_response) as mock_get:
user = client.get_user(1)
assert user["id"] == 1
assert user["name"] == "John Doe"
mock_get.assert_called_once_with("https://api.example.com/users/1")
def test_get_user_not_found():
"""Test API call with 404 error."""
client = APIClient("https://api.example.com")
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
with patch("requests.get", return_value=mock_response):
with pytest.raises(requests.HTTPError):
client.get_user(999)
@patch("requests.post")
def test_create_user(mock_post):
"""Test user creation with decorator syntax."""
client = APIClient("https://api.example.com")
mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}
mock_post.return_value.raise_for_status.return_value = None
user_data = {"name": "Jane Doe", "email": "jane@example.com"}
result = client.create_user(user_data)
assert result["id"] == 2
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["json"] == user_data
```
### Pattern 5: Testing Exceptions
```python
# test_exceptions.py
import pytest
def divide(a: float, b: float) -> float:
"""Divide a by b."""
if b == 0:
raise ZeroDivisionError("Division by zero")
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Arguments must be numbers")
return a / b
def test_zero_division():
"""Test exception is raised for division by zero."""
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_zero_division_with_message():
"""Test exception message."""
with pytest.raises(ZeroDivisionError, match="Division by zero"):
divide(5, 0)
def test_type_error():
"""Test type error exception."""
with pytest.raises(TypeError, match="must be numbers"):
divide("10", 5)
def test_exception_info():
"""Test accessing exception info."""
with pytest.raises(ValueError) as exc_info:
int("not a number")
assert "invalid literal" in str(exc_info.value)
```
For advanced patterns including async testing, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration files, see [references/advanced-patterns.md](references/advanced-patterns.md)
## Test Design Principles
### One Behavior Per Test
Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.
```python
# BAD - testing multiple behaviors
def test_user_service():
user = service.create_user(data)
assert user.id is not None
assert user.email == data["email"]
updated = service.update_user(user.id, {"name": "New"})
assert updated.name == "New"
# GOOD - focused tests
def test_create_user_assigns_id():
user = service.create_user(data)
assert user.id is not None
def test_create_user_stores_email():
user = service.create_user(data)
assert user.email == data["email"]
def test_update_user_changes_name():
user = service.create_user(data)
updated = service.update_user(user.id, {"name": "New"})
assert updated.name == "New"
```
### Test Error Paths
Always test failure cases, not just happy paths.
```python
def test_get_user_raises_not_found():
with pytest.raises(UserNotFoundError) as exc_info:
service.get_user("nonexistent-id")
assert "nonexistent-id" in str(exc_info.value)
def test_create_user_rejects_invalid_email():
with pytest.raises(ValueError, match="Invalid email format"):
service.create_user({"email": "not-an-email"})
```
## Testing Best Practices
### Test Organization
```python
# tests/
# __init__.py
# conftest.py # Shared fixtures
# test_unit/ # Unit tests
# test_models.py
# test_utils.py
# test_integration/ # Integration tests
# test_api.py
# test_database.py
# test_e2e/ # End-to-end tests
# test_workflows.py
```
### Test Naming Convention
A common pattern: `test_<unit>_<scenario>_<expected_outcome>`. Adapt to your team's preferences.
```python
# Pattern: test_<unit>_<scenario>_<expected>
def test_create_user_with_valid_data_returns_user():
...
def test_create_user_with_duplicate_email_raises_conflict():
...
def test_get_user_with_unknown_id_returns_none():
...
# Good test names - clear and descriptive
def test_user_creation_with_valid_data():
"""Clear name describes what is being tested."""
pass
def test_login_fails_with_invalid_password():
"""Name describes expected behavior."""
pass
def test_api_returns_404_for_missing_resource():
"""Specific about inputs and expected outcomes."""
pass
# Bad test names - avoid these
def test_1(): # Not descriptive
pass
def test_user(): # Too vague
pass
def test_function(): # Doesn't explain what's tested
pass
```
### Testing Retry Behavior
Verify that retry logic works correctly using mock side effects.
```python
from unittest.mock import Mock
def test_retries_on_transient_error():
"""Test that service retries on transient failures."""
client = Mock()
# Fail twice, then succeed
client.request.side_effect = [
ConnectionError("Failed"),
ConnectionError("Failed"),
{"status": "ok"},
]
service = ServiceWithRetry(client, max_retries=3)
result = service.fetch()
assert result == {"status": "ok"}
assert client.request.call_count == 3
def test_gives_up_after_max_retries():
"""Test that service stops retrying after max attempts."""
client = Mock()
client.request.side_effect = ConnectionError("Failed")
service = ServiceWithRetry(client, max_retries=3)
with pytest.raises(ConnectionError):
service.fetch()
assert client.request.call_count == 3
def test_does_not_retry_on_permanent_error():
"""Test that permanent errors are not retried."""
client = Mock()
client.request.side_effect = ValueError("Invalid input")
service = ServiceWithRetry(client, max_retries=3)
with pytest.raises(ValueError):
service.fetch()
# Only called once - no retry for ValueError
assert client.request.call_count == 1
```
### Mocking Time with Freezegun
Use freezegun to control time in tests for predictable time-dependent behavior.
```python
from freezegun import freeze_time
from datetime import datetime, timedelta
@freeze_time("2026-01-15 10:00:00")
def test_token_expiry():
"""Test token expires at correct time."""
token = create_token(expires_in_seconds=3600)
assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)
@freeze_time("2026-01-15 10:00:00")
def test_is_expired_returns_false_before_expiry():
"""Test token is not expired when within validity period."""
token = create_token(expires_in_seconds=3600)
assert not token.is_expired()
@freeze_time("2026-01-15 12:00:00")
def test_is_expired_returns_true_after_expiry():
"""Test token is expired after validity period."""
token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
assert token.is_expired()
def test_with_time_travel():
"""Test behavior across time using freeze_time context."""
with freeze_time("2026-01-01") as frozen_time:
item = create_item()
assert item.created_at == datetime(2026, 1, 1)
# Move forward in time
frozen_time.move_to("2026-01-15")
assert item.age_days == 14
```
### Test Markers
```python
# test_markers.py
import pytest
@pytest.mark.slow
def test_slow_operation():
"""Mark slow tests."""
import time
time.sleep(2)
@pytest.mark.integration
def test_database_integration():
"""Mark integration tests."""
pass
@pytest.mark.skip(reason="Feature not implemented yet")
def test_future_feature():
"""Skip tests temporarily."""
pass
@pytest.mark.skipif(os.name == "nt", reason="Unix only test")
def test_unix_specific():
"""Conditional skip."""
pass
@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
"""Mark expected failures."""
assert False
# Run with:
# pytest -m slow # Run only slow tests
# pytest -m "not slow" # Skip slow tests
# pytest -m integration # Run integration tests
```
### Coverage Reporting
```bash
# Install coverage
pip install pytest-cov
# Run tests with coverage
pytest --cov=myapp tests/
# Generate HTML report
pytest --cov=myapp --cov-report=html tests/
# Fail if coverage below threshold
pytest --cov=myapp --cov-fail-under=80 tests/
# Show missing lines
pytest --cov=myapp --cov-report=term-missing tests/
```
For advanced patterns (async testing, monkeypatching, property-based testing, database testing, CI/CD integration, and configuration), see [references/advanced-patterns.md](references/advanced-patterns.md)Related Skills
python-type-safety
Python type safety with type hints, generics, protocols, and strict type checking. Use when adding type annotations, implementing generic classes, defining structural interfaces, or configuring mypy/pyright.
python-resource-management
Python resource management with context managers, cleanup patterns, and streaming. Use when managing connections, file handles, implementing cleanup logic, or building streaming responses with accumulated state.
python-resilience
Python resilience patterns including automatic retries, exponential backoff, timeouts, and fault-tolerant decorators. Use when adding retry logic, implementing timeouts, building fault-tolerant services, or handling transient failures.
python-project-structure
Python project organization, module architecture, and public API design. Use when setting up new projects, organizing modules, defining public interfaces with __all__, or planning directory layouts.
python-performance-optimization
Profile and optimize Python code using cProfile, memory profilers, and performance best practices. Use when debugging slow Python code, optimizing bottlenecks, or improving application performance.
python-packaging
Create distributable Python packages with proper project structure, setup.py/pyproject.toml, and publishing to PyPI. Use when packaging Python libraries, creating CLI tools, or distributing Python code.
python-observability
Python observability patterns including structured logging, metrics, and distributed tracing. Use when adding logging, implementing metrics collection, setting up tracing, or debugging production systems.
python-error-handling
Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.
python-design-patterns
Python design patterns including KISS, Separation of Concerns, Single Responsibility, and composition over inheritance. Use this skill when designing a new service or component from scratch and choosing how to layer responsibilities, when refactoring a God class or monolithic function that has grown too large, when deciding whether to add a new abstraction or live with duplication, when evaluating a pull request for structural issues like tight coupling or leaking internal types, when choosing between inheritance and composition for a new class hierarchy, or when a codebase is becoming hard to test because of entangled I/O and business logic.
python-configuration
Python configuration management via environment variables and typed settings. Use when externalizing config, setting up pydantic-settings, managing secrets, or implementing environment-specific behavior.
python-code-style
Python code style, linting, formatting, naming conventions, and documentation standards. Use when writing new code, reviewing style, configuring linters, writing docstrings, or establishing project standards.
python-background-jobs
Python background job patterns including task queues, workers, and event-driven architecture. Use when implementing async task processing, job queues, long-running operations, or decoupling work from request/response cycles.