
Python Testing Patterns
- 7 installs
- 28 repo stars
- Updated June 29, 2026
- nickcrew/claude-cortex
This is a copy of python-testing-patterns by nickcrew - installs and ranking accrue to the original listing.
Helps with testing & qa tasks.
About
python-testing-patterns is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- python-testing-patterns
- Testing & QA
- AI-coding skill
Python Testing Patterns by the numbers
- 7 all-time installs (skills.sh)
- +2 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nickcrew/claude-cortex --skill python-testing-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 28 |
| Last updated | June 29, 2026 |
| Repository | nickcrew/claude-cortex ↗ |
What it does
Helps with testing & qa tasks.
Files
Python Testing Patterns
Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and property-based testing.
When to Use This Skill
- Writing unit tests for Python functions and classes
- Setting up comprehensive test suites and infrastructure
- Implementing test-driven development (TDD) workflows
- Creating integration tests for APIs, databases, and services
- Mocking external dependencies and third-party services
- Testing async code and concurrent operations
- Implementing property-based testing with Hypothesis
- Setting up CI/CD test automation
- Debugging failing tests and improving test coverage
Core Concepts
Test Discovery: Files matching test_*.py or *_test.py, functions starting with test_
Fixtures: Reusable test resources with setup and teardown
- Scopes:
function(default),class,module,session - Composition: Build complex fixtures from simple ones
- Share via
conftest.pyfor project-wide availability
Assertions: Use assert statements, pytest.raises() for exceptions
Organization: Separate unit/, integration/, e2e/ directories
Quick Reference
Load detailed references for specific topics:
| Task | Reference File |
|---|---|
| Pytest basics, test structure, AAA pattern | skills/python-testing-patterns/references/pytest-fundamentals.md |
| Fixtures, scopes, setup/teardown, conftest.py | skills/python-testing-patterns/references/fixtures.md |
| Parametrization, multiple test cases | skills/python-testing-patterns/references/parametrized-tests.md |
| Mocking, patching, unittest.mock, pytest-mock | skills/python-testing-patterns/references/mocking.md |
| Async tests, pytest-asyncio, event loops | skills/python-testing-patterns/references/async-testing.md |
| Property-based testing, Hypothesis, strategies | skills/python-testing-patterns/references/property-based-testing.md |
| Monkeypatch, environment variables, attributes | skills/python-testing-patterns/references/monkeypatch.md |
| Test structure, markers, conftest.py patterns | skills/python-testing-patterns/references/test-organization.md |
| Coverage measurement, reports, thresholds | skills/python-testing-patterns/references/coverage.md |
| Database, API, Redis, message queue testing | skills/python-testing-patterns/references/integration-testing.md |
| Best practices, test quality, fixture design | skills/python-testing-patterns/references/best-practices.md |
Workflow
1. Basic Test Setup
# test_example.py
import pytest
def test_something():
"""Descriptive test name."""
# Arrange
expected = 5
# Act
result = 2 + 3
# Assert
assert result == expectedRun tests:
pytest # Run all tests
pytest -v # Verbose output
pytest tests/unit/ # Specific directory
pytest -k "test_user" # Match pattern
pytest -m unit # Run marked tests2. Using Fixtures
@pytest.fixture
def sample_data():
"""Provide test data."""
data = {"key": "value"}
yield data
# Cleanup if needed
def test_with_fixture(sample_data):
assert sample_data["key"] == "value"3. Parametrized Tests
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(4, 16),
])
def test_square(input, expected):
assert input ** 2 == expected4. Mocking External Dependencies
from unittest.mock import patch
@patch("module.external_api_call")
def test_with_mock(mock_api):
mock_api.return_value = {"status": "ok"}
result = my_function()
assert result["status"] == "ok"
mock_api.assert_called_once()5. Coverage Measurement
pytest --cov=src --cov-report=term-missing
pytest --cov=src --cov-report=html
pytest --cov=src --cov-fail-under=806. Test Configuration
pytest.ini:
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -v --strict-markers --cov=src
markers =
unit: Unit tests
integration: Integration tests
slow: Slow testsCommon Patterns
Exception testing:
with pytest.raises(ValueError, match="error message"):
function_that_raises()Async testing:
@pytest.mark.asyncio
async def test_async_function():
result = await async_operation()
assert result is not NoneTemporary files:
def test_file_operation(tmp_path):
test_file = tmp_path / "test.txt"
test_file.write_text("content")
assert test_file.read_text() == "content"Markers for test selection:
@pytest.mark.slow
@pytest.mark.integration
def test_database_operation():
passCommon Mistakes
1. Not using fixtures: Repeating setup code across tests
- Solution: Create fixtures in conftest.py
2. Tests depending on order: Global state pollution
- Solution: Ensure test independence with proper fixtures
3. Over-mocking: Mocking internal implementation
- Solution: Mock only external boundaries (APIs, databases)
4. Missing edge cases: Only testing happy path
- Solution: Test boundary conditions, errors, and invalid inputs
5. Slow tests: Running full integration tests frequently
- Solution: Separate unit/integration, use markers, optimize fixtures
6. Ignoring coverage gaps: Not measuring test coverage
- Solution: Use pytest-cov and track metrics
7. Poor test names: Generic names like test_1()
- Solution: Use descriptive names:
test_<behavior>_<condition>_<expected>
8. No cleanup: Resources not released
- Solution: Use fixtures with proper teardown (yield pattern)
Resources
- pytest: https://docs.pytest.org/
- unittest.mock: https://docs.python.org/3/library/unittest.mock.html
- pytest-asyncio: Testing async code
- pytest-cov: Coverage reporting
- pytest-mock: pytest wrapper for mock
- Hypothesis: https://hypothesis.readthedocs.io/
- pytest-xdist: Parallel test execution
- testcontainers: Docker containers for testing
Testing Async Code
Basic Async Tests
Test coroutines and async operations:
import pytest
import asyncio
async def fetch_data(url: str) -> dict:
await asyncio.sleep(0.1)
return {"url": url, "data": "result"}
@pytest.mark.asyncio
async def test_fetch_data():
"""Test async function."""
result = await fetch_data("https://api.example.com")
assert result["url"] == "https://api.example.com"Setup
Install pytest-asyncio:
pip install pytest-asyncioConfigure in pytest.ini:
[pytest]
asyncio_mode = autoConcurrent Operations
Test multiple async operations:
@pytest.mark.asyncio
async def test_concurrent_operations():
"""Test multiple async operations."""
urls = ["url1", "url2", "url3"]
tasks = [fetch_data(url) for url in urls]
results = await asyncio.gather(*tasks)
assert len(results) == 3
assert all("data" in r for r in results)Async Fixtures
Create async fixtures:
@pytest.fixture
async def async_client():
"""Async fixture with setup and teardown."""
client = {"connected": False}
# Setup
client["connected"] = True
yield client
# Teardown
client["connected"] = False
@pytest.mark.asyncio
async def test_with_async_fixture(async_client):
"""Use async fixture."""
assert async_client["connected"] is TrueTesting Timeouts
Verify timeout behavior:
@pytest.mark.asyncio
async def test_timeout():
"""Test operation with timeout."""
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(long_running_task(), timeout=0.1)Testing Exception Handling
Async exceptions:
async def failing_operation():
await asyncio.sleep(0.1)
raise ValueError("Async error")
@pytest.mark.asyncio
async def test_async_exception():
"""Test async exception handling."""
with pytest.raises(ValueError, match="Async error"):
await failing_operation()Mocking Async Functions
Mock async calls:
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_with_async_mock(mocker):
"""Mock async function."""
mock_fetch = AsyncMock(return_value={"data": "mocked"})
mocker.patch("module.fetch_data", mock_fetch)
result = await module.fetch_data("url")
assert result["data"] == "mocked"
mock_fetch.assert_awaited_once_with("url")Event Loop Management
Custom event loop:
@pytest.fixture
def event_loop():
"""Create custom event loop."""
loop = asyncio.new_event_loop()
yield loop
loop.close()Testing Async Context Managers
class AsyncResource:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.cleanup()
async def cleanup(self):
pass
@pytest.mark.asyncio
async def test_async_context_manager():
"""Test async context manager."""
async with AsyncResource() as resource:
assert resource is not NoneBest Practices
1. Use pytest-asyncio: Don't write manual event loop code 2. Mark tests: Always use @pytest.mark.asyncio 3. Async all the way: Don't mix sync/async in fixtures 4. Test concurrency: Verify parallel behavior explicitly 5. Timeout protection: Add timeouts to prevent hanging tests 6. AsyncMock: Use for mocking async functions 7. Clean event loops: Ensure proper cleanup
Testing Best Practices
Test Quality Principles
1. One Concept Per Test
Each test should verify a single behavior:
# Good: Tests one concept
def test_user_creation_sets_default_role():
user = User(name="Test")
assert user.role == "user"
def test_user_creation_generates_unique_id():
user = User(name="Test")
assert user.id is not None
# Bad: Tests multiple concepts
def test_user_creation():
user = User(name="Test")
assert user.role == "user" # Concept 1
assert user.id is not None # Concept 2
assert user.created_at is not None # Concept 32. Descriptive Test Names
Use pattern: `test_<behavior>_<condition>_<expected>`:
# Good: Clear what is being tested
def test_email_validation_with_invalid_format_returns_false():
assert not is_valid_email("invalid")
def test_divide_by_zero_raises_value_error():
with pytest.raises(ValueError):
divide(5, 0)
def test_user_creation_with_duplicate_email_raises_integrity_error():
create_user("test@example.com")
with pytest.raises(IntegrityError):
create_user("test@example.com")
# Bad: Unclear purpose
def test_email():
assert not is_valid_email("invalid")
def test_divide():
with pytest.raises(ValueError):
divide(5, 0)3. AAA Pattern (Arrange-Act-Assert)
Structure tests consistently:
def test_user_registration_sends_welcome_email():
# Arrange
user_data = {"email": "test@example.com", "name": "Test"}
mock_email = Mock()
# Act
user = register_user(user_data, email_service=mock_email)
# Assert
assert user.email == "test@example.com"
mock_email.send_welcome.assert_called_once_with(user)4. Test Independence
Tests should not depend on each other:
# Good: Each test is independent
def test_create_user():
user = User(name="Test")
assert user.name == "Test"
def test_update_user():
user = User(name="Original")
user.name = "Updated"
assert user.name == "Updated"
# Bad: Tests depend on order
test_user = None
def test_create_user_step1():
global test_user
test_user = User(name="Test") # Modifies global state
def test_update_user_step2():
global test_user
test_user.name = "Updated" # Depends on step15. Deterministic Tests
Same input always produces same result:
# Good: Deterministic
def test_addition():
assert add(2, 3) == 5 # Always true
# Bad: Non-deterministic
def test_random_generation():
import random
value = random.randint(1, 10)
assert value > 0 # Could fail randomly
# Fix: Control randomness
def test_random_generation_fixed():
import random
random.seed(42) # Fixed seed
value = random.randint(1, 10)
assert value == 2 # DeterministicFixture Design
6. Appropriate Scope
Use narrowest scope needed:
# Function scope: Clean state per test (default)
@pytest.fixture
def user():
return User(name="Test")
# Module scope: Expensive setup
@pytest.fixture(scope="module")
def database_connection():
conn = create_connection()
yield conn
conn.close()
# Session scope: Once per test run
@pytest.fixture(scope="session")
def app_config():
return load_config()7. Fixture Composition
Build complex fixtures from simple ones:
@pytest.fixture
def database_url():
return "postgresql://localhost/test"
@pytest.fixture
def database_engine(database_url):
return create_engine(database_url)
@pytest.fixture
def database_session(database_engine):
Session = sessionmaker(bind=database_engine)
session = Session()
yield session
session.close()8. Resource Cleanup
Always clean up in teardown:
@pytest.fixture
def temp_file():
# Setup
file = Path("temp.txt")
file.write_text("test")
yield file
# Teardown - always runs
if file.exists():
file.unlink()9. Fixture Reusability
Share common fixtures in conftest.py:
# tests/conftest.py
@pytest.fixture
def sample_user():
"""Available to all tests."""
return User(name="Test", email="test@example.com")
@pytest.fixture
def authenticated_client(client, sample_user):
"""Client with authentication."""
token = create_token(sample_user)
client.headers = {"Authorization": f"Bearer {token}"}
return client10. Clear Fixture Names
Descriptive names explain purpose:
# Good: Clear purpose
@pytest.fixture
def user_with_admin_role():
return User(name="Admin", role="admin")
@pytest.fixture
def database_session_with_rollback():
# ...
# Bad: Unclear purpose
@pytest.fixture
def u():
return User(name="Admin")
@pytest.fixture
def db():
# What kind of db? What state?Mocking Strategy
11. Mock at Boundaries
Mock external systems, test internal logic:
# Good: Mock external API
def test_fetch_user_data():
with patch("requests.get") as mock_get:
mock_get.return_value.json.return_value = {"id": 1}
result = fetch_user_data(1)
assert result["id"] == 1
# Bad: Over-mocking
def test_process_user():
with patch("module.User") as MockUser: # Don't mock own code
with patch("module.validate") as mock_validate:
# Too much mocking12. Don't Over-Mock
Test real code when possible:
# Good: Test real implementation
def test_calculate_total():
items = [{"price": 10}, {"price": 20}]
assert calculate_total(items) == 30
# Bad: Mock everything
def test_calculate_total_with_mocks():
mock_items = Mock()
mock_items.return_value = 30 # Why test a mock?13. Verify Interactions
Assert mocks were called correctly:
def test_user_creation_sends_email():
mock_email = Mock()
create_user("test@example.com", email_service=mock_email)
# Verify
mock_email.send_welcome.assert_called_once()
mock_email.send_welcome.assert_called_with(
email="test@example.com"
)14. Reset Mocks
Ensure clean state between tests:
@pytest.fixture
def mock_service():
mock = Mock()
yield mock
mock.reset_mock() # Clean up15. Define Return Values
Always specify expected returns:
# Good: Clear return value
mock_api = Mock()
mock_api.get_user.return_value = {"id": 1, "name": "Test"}
# Bad: Undefined return
mock_api = Mock()
result = mock_api.get_user() # Returns Mock, not dictTest Organization
16. Parallel Structure
Mirror source code organization:
src/
├── models.py
├── services.py
└── utils.py
tests/
├── test_models.py
├── test_services.py
└── test_utils.py17. Test Categorization
Use markers for organization:
@pytest.mark.unit
def test_pure_function():
pass
@pytest.mark.integration
@pytest.mark.database
def test_database_operation():
pass
@pytest.mark.e2e
@pytest.mark.slow
def test_workflow():
pass18. Configuration Management
Centralize test configuration:
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -v --strict-markers
markers =
unit: Unit tests
integration: Integration tests
slow: Slow tests19. Separate Concerns
Different directories for different test types:
tests/
├── unit/ # Fast, isolated
├── integration/ # Component interaction
└── e2e/ # Full workflows20. Fast by Default
Optimize for quick feedback:
# Quick run: unit tests only
pytest tests/unit/
# Full run: all tests
pytest
# Slow tests: run less frequently
pytest -m slowCoverage Philosophy
21. Measure Coverage
pytest --cov=src --cov-report=term-missing22. Quality Over Quantity
100% coverage ≠ bug-free code
Focus on:
- Critical business logic
- Edge cases and error handling
- Complex algorithms
- Security-sensitive code
23. Prioritize Critical Paths
Test important code thoroughly:
- User authentication
- Payment processing
- Data validation
- Security checks
24. Test Edge Cases
Boundary conditions and errors:
def test_divide_edge_cases():
# Normal case
assert divide(10, 2) == 5
# Edge: zero
with pytest.raises(ValueError):
divide(10, 0)
# Edge: negative
assert divide(-10, 2) == -5
# Edge: float precision
assert abs(divide(1, 3) - 0.333333) < 0.0000125. Continuous Monitoring
Track coverage in CI/CD:
# GitHub Actions
- name: Run tests with coverage
run: pytest --cov=src --cov-fail-under=80Coverage and Quality Metrics
Installation
pip install pytest-covBasic Coverage
Run tests with coverage:
# Basic coverage report
pytest --cov=src tests/
# Show missing lines
pytest --cov=src --cov-report=term-missing tests/
# HTML report
pytest --cov=src --cov-report=html tests/
open htmlcov/index.html
# XML report (for CI)
pytest --cov=src --cov-report=xml tests/
# Fail if below threshold
pytest --cov=src --cov-fail-under=80 tests/Configuration
pytest.ini
[pytest]
addopts =
--cov=src
--cov-report=term-missing
--cov-report=html
--cov-fail-under=80.coveragerc
[run]
source = src
omit =
*/tests/*
*/test_*.py
*/__pycache__/*
*/venv/*
[report]
precision = 2
show_missing = True
skip_covered = False
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethodpyproject.toml
[tool.coverage.run]
source = ["src"]
omit = [
"*/tests/*",
"*/test_*.py",
"*/__pycache__/*",
"*/venv/*",
]
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]Excluding Code from Coverage
Pragma comments:
def important_function():
result = calculate()
if DEBUG: # pragma: no cover
print(f"Debug: {result}")
return result
def __repr__(self): # pragma: no cover
return f"<User {self.name}>"Type checking blocks:
from typing import TYPE_CHECKING
if TYPE_CHECKING: # Automatically excluded
from .models import UserBranch Coverage
Track both line and branch coverage:
pytest --cov=src --cov-branch tests/Example:
def check_value(x):
if x > 0:
return "positive"
else:
return "non-positive"
# Line coverage: 100% if function called once
# Branch coverage: 100% only if both branches tested
def test_positive():
assert check_value(5) == "positive"
def test_non_positive():
assert check_value(0) == "non-positive"Coverage Reports
Terminal Report
pytest --cov=src --cov-report=term-missingOutput:
---------- coverage: platform linux, python 3.11 -----------
Name Stmts Miss Cover Missing
-------------------------------------------------
src/models.py 45 2 96% 23, 67
src/services.py 78 5 94% 45-49
src/utils.py 23 0 100%
-------------------------------------------------
TOTAL 146 7 95%HTML Report
pytest --cov=src --cov-report=htmlBenefits:
- Visual line-by-line coverage
- Branch coverage visualization
- Missing line highlighting
- Interactive navigation
XML Report (CI/CD)
pytest --cov=src --cov-report=xmlFor integration with:
- GitHub Actions
- GitLab CI
- Jenkins
- SonarQube
- Codecov
- Coveralls
Coverage Thresholds
Enforce minimum coverage:
# Fail if below 80%
pytest --cov=src --cov-fail-under=80Per-file thresholds in .coveragerc:
[coverage:report]
fail_under = 80
[coverage:paths]
source = src/
[coverage:run]
# Per-module configuration
[src/critical.py]
fail_under = 95
[src/utils.py]
fail_under = 90Incremental Coverage
Only check coverage of changed files:
# Using coverage.py directly
coverage run -m pytest
coverage report --skip-coveredCoverage in CI/CD
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests with coverage
run: pytest --cov=src --cov-report=xml --cov-fail-under=80
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
files: ./coverage.xml
fail_ci_if_error: trueQuality Metrics Beyond Coverage
Test Count
pytest --collect-only | grep "test session starts"Test Duration
pytest --durations=10 # Show 10 slowest testsTest Distribution
pytest --markers # Show all available markers
pytest -m unit --collect-only # Count unit tests
pytest -m integration --collect-only # Count integration testsInterpreting Coverage
Coverage != Quality
- 100% coverage doesn't guarantee bug-free code
- Low coverage indicates untested code
- High coverage is necessary but not sufficient
Good Coverage Targets
- Overall: 80-90%
- Critical code: 95-100%
- Utility code: 90-100%
- UI code: 60-80%
- Integration glue: 70-85%
Focus Areas
1. Critical paths: Business logic, security, data integrity 2. Edge cases: Boundary conditions, error handling 3. Complex code: High cyclomatic complexity 4. Frequently changed: Code that changes often
Best Practices
1. Track trends: Monitor coverage over time 2. Prevent regression: Don't let coverage decrease 3. Quality over quantity: Meaningful tests, not just coverage 4. Focus on untested: Use reports to find gaps 5. Exclude appropriately: Don't test unreachable code 6. Branch coverage: More valuable than line coverage 7. CI enforcement: Fail builds on coverage drops 8. Team visibility: Share coverage reports 9. Incremental improvement: Gradually increase coverage 10. Document exclusions: Comment why code is excluded
Fixtures for Setup and Teardown
Basic Fixtures
Reusable test resources with cleanup:
import pytest
from typing import Generator
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.connected = False
def connect(self):
self.connected = True
def disconnect(self):
self.connected = False
@pytest.fixture
def db() -> Generator[Database, None, None]:
"""Fixture providing database connection."""
# Setup
database = Database("sqlite:///:memory:")
database.connect()
yield database # Provide to test
# Teardown
database.disconnect()
def test_database_connection(db):
"""Test using fixture."""
assert db.connected is TrueFixture Scopes
@pytest.fixture(scope="function") # Default - per test function
def per_test_resource():
return {"data": "fresh"}
@pytest.fixture(scope="class") # Per test class
def per_class_resource():
return {"shared": "class-level"}
@pytest.fixture(scope="module") # Per test module
def per_module_resource():
return {"shared": "module-level"}
@pytest.fixture(scope="session") # Once per test session
def per_session_resource():
return {"shared": "session-level"}Scope ordering: function < class < module < session
Fixture Composition
Build complex fixtures from simple ones:
@pytest.fixture
def database_url():
return "postgresql://localhost/test_db"
@pytest.fixture
def database_engine(database_url):
"""Depends on database_url fixture."""
from sqlalchemy import create_engine
return create_engine(database_url)
@pytest.fixture
def database_session(database_engine):
"""Depends on database_engine fixture."""
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=database_engine)
session = Session()
yield session
session.close()Autouse Fixtures
Automatically run for every test:
@pytest.fixture(autouse=True)
def reset_state():
"""Auto-run cleanup before each test."""
# Setup
global_state.clear()
yield
# Teardown
global_state.clear()Shared Fixtures in conftest.py
tests/conftest.py:
import pytest
@pytest.fixture(scope="session")
def app_config():
"""Available to all tests."""
return {
"debug": True,
"api_key": "test-key",
"database": "sqlite:///:memory:"
}
@pytest.fixture
def sample_user():
"""Sample user data for tests."""
return {
"id": 1,
"name": "Test User",
"email": "test@example.com"
}Parametrized Fixtures
Generate multiple fixture instances:
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_type(request):
"""Run tests with different database types."""
return request.param
def test_with_all_databases(database_type):
"""This test runs 3 times, once per database."""
assert database_type in ["sqlite", "postgres", "mysql"]Integration Testing Patterns
Database Testing
Testing with SQLAlchemy:
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base, User
@pytest.fixture(scope="function")
def db_session():
"""Provide clean database session per test."""
# Create in-memory database
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
@pytest.fixture(scope="function")
def db_session_with_rollback(database_engine):
"""Session with automatic rollback."""
connection = database_engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
def test_user_creation(db_session):
"""Test creating user in database."""
user = User(name="Test User", email="test@example.com")
db_session.add(user)
db_session.commit()
assert user.id is not None
# Query to verify
retrieved = db_session.query(User).filter_by(email="test@example.com").first()
assert retrieved.name == "Test User"
def test_user_query(db_session):
"""Test querying users."""
# Insert test data
users = [
User(name="Alice", email="alice@example.com"),
User(name="Bob", email="bob@example.com"),
]
db_session.add_all(users)
db_session.commit()
# Query
results = db_session.query(User).all()
assert len(results) == 2
def test_user_update(db_session):
"""Test updating user."""
user = User(name="Test", email="test@example.com")
db_session.add(user)
db_session.commit()
user.name = "Updated"
db_session.commit()
retrieved = db_session.query(User).filter_by(id=user.id).first()
assert retrieved.name == "Updated"API Endpoint Testing
FastAPI
import pytest
from fastapi.testclient import TestClient
from myapp import app
@pytest.fixture
def client():
"""Provide test client."""
return TestClient(app)
def test_get_user(client):
"""Test GET /users/{id} endpoint."""
response = client.get("/users/1")
assert response.status_code == 200
data = response.json()
assert "id" in data
assert "name" in data
def test_create_user(client):
"""Test POST /users endpoint."""
user_data = {
"name": "New User",
"email": "new@example.com"
}
response = client.post("/users", json=user_data)
assert response.status_code == 201
assert response.json()["email"] == "new@example.com"
def test_authentication_required(client):
"""Test endpoint requires authentication."""
response = client.get("/protected")
assert response.status_code == 401
def test_with_authentication(client):
"""Test authenticated request."""
headers = {"Authorization": "Bearer test-token"}
response = client.get("/protected", headers=headers)
assert response.status_code == 200Flask
import pytest
from myapp import create_app
@pytest.fixture
def app():
"""Create test app."""
app = create_app(testing=True)
yield app
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
def test_index_page(client):
"""Test index page."""
response = client.get("/")
assert response.status_code == 200
assert b"Welcome" in response.data
def test_post_data(client):
"""Test POST request."""
response = client.post("/api/data", json={"key": "value"})
assert response.status_code == 200
assert response.json["key"] == "value"Redis Testing
Testing with Redis:
import pytest
import redis
from fakeredis import FakeRedis
@pytest.fixture
def redis_client():
"""Provide fake Redis client for testing."""
client = FakeRedis()
yield client
client.flushall()
def test_set_get(redis_client):
"""Test Redis set and get."""
redis_client.set("key", "value")
assert redis_client.get("key") == b"value"
def test_expiration(redis_client):
"""Test key expiration."""
redis_client.setex("key", 1, "value")
assert redis_client.get("key") == b"value"
# Wait for expiration
import time
time.sleep(2)
assert redis_client.get("key") is NoneMessage Queue Testing
Testing with Celery:
import pytest
from celery import Celery
@pytest.fixture
def celery_app():
"""Create Celery app for testing."""
app = Celery(broker="memory://", backend="cache+memory://")
app.conf.task_always_eager = True # Run tasks synchronously
return app
def test_task_execution(celery_app):
"""Test Celery task."""
@celery_app.task
def add(x, y):
return x + y
result = add.delay(2, 3)
assert result.get() == 5External Service Mocking
Testing with httpx:
import pytest
import httpx
from respx import MockRouter
@pytest.fixture
def mock_api():
"""Mock external API."""
with MockRouter() as router:
router.get("https://api.example.com/users/1").mock(
return_value=httpx.Response(
200,
json={"id": 1, "name": "Test User"}
)
)
yield router
def test_external_api_call(mock_api):
"""Test calling external API."""
response = httpx.get("https://api.example.com/users/1")
assert response.status_code == 200
assert response.json()["name"] == "Test User"File System Testing
Testing file operations:
import pytest
from pathlib import Path
def test_file_creation(tmp_path):
"""Test creating and reading files."""
test_file = tmp_path / "test.txt"
test_file.write_text("Hello, World!")
assert test_file.exists()
assert test_file.read_text() == "Hello, World!"
def test_directory_operations(tmp_path):
"""Test directory operations."""
subdir = tmp_path / "subdir"
subdir.mkdir()
(subdir / "file1.txt").write_text("Content 1")
(subdir / "file2.txt").write_text("Content 2")
files = list(subdir.iterdir())
assert len(files) == 2Docker Test Containers
Testing with testcontainers:
import pytest
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="module")
def postgres_container():
"""Provide PostgreSQL container."""
with PostgresContainer("postgres:14") as postgres:
yield postgres
def test_with_postgres(postgres_container):
"""Test with real PostgreSQL."""
import psycopg2
conn = psycopg2.connect(postgres_container.get_connection_url())
cursor = conn.cursor()
cursor.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR)")
cursor.execute("INSERT INTO users (name) VALUES ('Test')")
conn.commit()
cursor.execute("SELECT name FROM users")
result = cursor.fetchone()
assert result[0] == "Test"
conn.close()Environment Setup
Testing with different configurations:
import pytest
import os
@pytest.fixture
def test_env(monkeypatch):
"""Set up test environment."""
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
monkeypatch.setenv("REDIS_URL", "redis://localhost:6379/1")
monkeypatch.setenv("DEBUG", "True")
def test_with_test_env(test_env):
"""Test with test environment."""
assert os.getenv("DATABASE_URL") == "postgresql://localhost/test"
assert os.getenv("DEBUG") == "True"Best Practices
1. Isolate tests: Each test should have clean state 2. Use transactions: Rollback database changes after tests 3. Mock external services: Don't hit real APIs in tests 4. Test containers: Use Docker for real dependencies 5. Seed data: Provide consistent test data 6. Fast setup: Optimize fixture creation time 7. Parallel execution: Tests should be independent 8. Clean teardown: Always clean up resources 9. Realistic data: Use data similar to production 10. Test error cases: Network failures, timeouts, etc.
Mocking with unittest.mock and pytest-mock
Basic Mocking
Isolate code from external dependencies:
import pytest
from unittest.mock import Mock, patch, MagicMock
import requests
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url
def get_user(self, user_id: int) -> dict:
response = requests.get(f"{self.base_url}/users/{user_id}")
response.raise_for_status()
return response.json()
def test_get_user_success():
"""Test with mock response."""
client = APIClient("https://api.example.com")
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "John"}
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
mock_get.assert_called_once_with("https://api.example.com/users/1")Mock Types
Mock
Basic mock object:
mock = Mock()
mock.return_value = 42
assert mock() == 42
mock.method.return_value = "result"
assert mock.method() == "result"MagicMock
Mock with magic methods:
mock = MagicMock()
mock.__len__.return_value = 5
assert len(mock) == 5
mock.__getitem__.return_value = "item"
assert mock[0] == "item"Patching
Context Manager Patching
with patch("module.function") as mock_func:
mock_func.return_value = "mocked"
result = module.function()
assert result == "mocked"Decorator Patching
@patch("module.function")
def test_something(mock_func):
mock_func.return_value = "mocked"
result = module.function()
assert result == "mocked"Object Patching
@patch.object(MyClass, "method")
def test_method(mock_method):
mock_method.return_value = "mocked"
obj = MyClass()
assert obj.method() == "mocked"Side Effects
Simulate exceptions or sequences:
# Raise exception
mock = Mock()
mock.side_effect = ValueError("Error message")
with pytest.raises(ValueError):
mock()
# Return sequence
mock = Mock()
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
# Custom function
def custom_behavior(x):
return x * 2
mock = Mock()
mock.side_effect = custom_behavior
assert mock(5) == 10Assertions
Verify mock interactions:
mock = Mock()
mock(1, 2, key="value")
# Called
assert mock.called
assert mock.call_count == 1
# Called with
mock.assert_called_once()
mock.assert_called_with(1, 2, key="value")
mock.assert_called_once_with(1, 2, key="value")
# Called with any args
mock.assert_called()
# Not called
mock_other = Mock()
mock_other.assert_not_called()pytest-mock Plugin
Cleaner syntax with pytest:
def test_with_mocker(mocker):
"""Using pytest-mock fixture."""
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"id": 2}
mock_get.return_value.raise_for_status.return_value = None
client = APIClient("https://api.example.com")
result = client.get_user(2)
assert result["id"] == 2Benefits:
- Automatic cleanup
- No context managers needed
- Integration with pytest fixtures
- Spy functionality
Best Practices
1. Mock at boundaries: Mock external systems (APIs, databases, files) 2. Don't over-mock: Test real code when possible 3. Verify interactions: Use assertions to verify calls 4. Clear return values: Always define expected return values 5. Reset between tests: Ensure clean state 6. Mock the interface: Mock at the lowest dependency level 7. Use spec: Mock(spec=ClassName) prevents invalid attribute access
Monkeypatch for Testing
Environment Variables
Modify environment safely:
import os
import pytest
def get_api_key() -> str:
return os.environ.get("API_KEY", "default-key")
def test_api_key_from_env(monkeypatch):
"""Test with custom environment variable."""
monkeypatch.setenv("API_KEY", "test-key-123")
assert get_api_key() == "test-key-123"
def test_api_key_default(monkeypatch):
"""Test default value."""
monkeypatch.delenv("API_KEY", raising=False)
assert get_api_key() == "default-key"Object Attributes
Modify object attributes temporarily:
class Config:
debug = False
timeout = 30
def test_monkeypatch_attribute(monkeypatch):
"""Modify object attributes."""
config = Config()
monkeypatch.setattr(config, "debug", True)
monkeypatch.setattr(config, "timeout", 60)
assert config.debug is True
assert config.timeout == 60Module Attributes
Replace module-level functions or constants:
import module
def test_monkeypatch_module_function(monkeypatch):
"""Replace module function."""
def mock_function():
return "mocked"
monkeypatch.setattr(module, "original_function", mock_function)
assert module.original_function() == "mocked"
def test_monkeypatch_constant(monkeypatch):
"""Replace module constant."""
monkeypatch.setattr(module, "MAX_RETRIES", 5)
assert module.MAX_RETRIES == 5Dictionary Items
Modify dictionaries:
def test_monkeypatch_dict(monkeypatch):
"""Modify dictionary items."""
config = {"key": "original"}
monkeypatch.setitem(config, "key", "modified")
monkeypatch.setitem(config, "new_key", "value")
assert config["key"] == "modified"
assert config["new_key"] == "value"
def test_delete_dict_item(monkeypatch):
"""Delete dictionary items."""
config = {"key": "value", "delete_me": "gone"}
monkeypatch.delitem(config, "delete_me")
assert "delete_me" not in configSystem Path
Modify sys.path:
import sys
def test_monkeypatch_syspath(monkeypatch):
"""Add to sys.path temporarily."""
monkeypatch.syspath_prepend("/custom/path")
assert "/custom/path" in sys.pathTime Mocking
Mock time and datetime:
from datetime import datetime
def test_mock_datetime(monkeypatch):
"""Mock datetime.now()."""
class MockDateTime:
@classmethod
def now(cls):
return datetime(2024, 1, 1, 12, 0, 0)
monkeypatch.setattr("datetime.datetime", MockDateTime)
assert datetime.now() == datetime(2024, 1, 1, 12, 0, 0)Working Directory
Change working directory:
def test_change_directory(monkeypatch, tmp_path):
"""Change working directory temporarily."""
monkeypatch.chdir(tmp_path)
assert os.getcwd() == str(tmp_path)Common Patterns
Mock External API Calls
def test_mock_requests(monkeypatch):
"""Mock requests library."""
class MockResponse:
@staticmethod
def json():
return {"key": "value"}
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr("requests.get", mock_get)
import requests
response = requests.get("https://api.example.com")
assert response.json() == {"key": "value"}Mock Database Connections
def test_mock_database(monkeypatch):
"""Mock database connection."""
class MockDB:
def query(self, sql):
return [{"id": 1, "name": "test"}]
monkeypatch.setattr("module.Database", MockDB)
db = module.Database()
results = db.query("SELECT * FROM users")
assert len(results) == 1Undo Changes
All changes automatically reverted after test:
def test_automatic_cleanup(monkeypatch):
"""Changes reverted after test."""
original_value = os.environ.get("TEST_VAR")
monkeypatch.setenv("TEST_VAR", "temporary")
assert os.environ["TEST_VAR"] == "temporary"
# After test completes, TEST_VAR reverts to original_valueBest Practices
1. Use for simple mocking: Monkeypatch is simpler than mock.patch for basic cases 2. Environment variables: Preferred method for env var mocking 3. Automatic cleanup: No need for teardown, monkeypatch handles it 4. Combine with fixtures: Use monkeypatch in fixtures for reusable mocking 5. Type safety: Be careful with type checkers, may need type: ignore 6. Integration with pytest: Native pytest fixture, well-integrated 7. Not for complex mocking: Use unittest.mock for complex scenarios
Parametrized Tests
Basic Parametrization
Test multiple inputs efficiently:
import pytest
def is_valid_email(email: str) -> bool:
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),
])
def test_email_validation(email, expected):
"""Test email validation with multiple cases."""
assert is_valid_email(email) == expectedCustom Test IDs
Make test output more readable:
@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):
assert (value > 0) == expectedOutput:
test_file.py::test_is_positive[positive] PASSED
test_file.py::test_is_positive[zero] PASSED
test_file.py::test_is_positive[negative] PASSEDMultiple Parameters
Test with multiple parameter sets:
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_addition(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expectedStacked Parametrization
Generate combinations:
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20])
def test_combinations(x, y):
"""Runs 6 times: (1,10), (1,20), (2,10), (2,20), (3,10), (3,20)"""
assert x * y > 0Parametrize from Files
Load test cases from external data:
import json
import pytest
def load_test_cases():
with open("test_cases.json") as f:
return json.load(f)
@pytest.mark.parametrize("case", load_test_cases())
def test_from_file(case):
input_data = case["input"]
expected = case["expected"]
assert process(input_data) == expectedBenefits
- DRY principle: Reduce test code duplication
- Coverage: Test edge cases systematically
- Readability: Clear input/output relationships
- Maintainability: Add new cases without new test functions
- Debugging: Failures show exact failing parameters
Property-Based Testing with Hypothesis
Core Concept
Instead of writing specific examples, define properties that should always hold:
from hypothesis import given, strategies as st
import pytest
def reverse_string(s: str) -> str:
return s[::-1]
@given(st.text())
def test_reverse_twice_returns_original(s):
"""Property: double reverse equals original."""
assert reverse_string(reverse_string(s)) == s
@given(st.text())
def test_reverse_preserves_length(s):
"""Property: length unchanged by reverse."""
assert len(reverse_string(s)) == len(s)Common Strategies
Basic Types
st.integers() # Any integer
st.integers(min_value=0) # Non-negative integers
st.floats() # Any float
st.text() # Unicode strings
st.booleans() # True/False
st.binary() # Bytes
st.none() # NoneCollections
st.lists(st.integers()) # List of integers
st.tuples(st.text(), st.integers()) # Tuple of (str, int)
st.dictionaries(st.text(), st.integers()) # Dict[str, int]
st.sets(st.text()) # Set of stringsConstrained Values
st.integers(min_value=1, max_value=100)
st.text(min_size=1, max_size=10)
st.text(alphabet="abc")
st.lists(st.integers(), min_size=1, max_size=5)Mathematical Properties
Test universal mathematical properties:
@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
"""Property: a + b = b + a."""
assert a + b == b + a
@given(st.integers(), st.integers(), st.integers())
def test_addition_associative(a, b, c):
"""Property: (a + b) + c = a + (b + c)."""
assert (a + b) + c == a + (b + c)
@given(st.integers())
def test_addition_identity(a):
"""Property: a + 0 = a."""
assert a + 0 == aList Properties
Test list operations:
@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(lst):
"""Property: sorted list is non-decreasing."""
sorted_lst = sorted(lst)
# Same length
assert len(sorted_lst) == len(lst)
# Is ordered
for i in range(len(sorted_lst) - 1):
assert sorted_lst[i] <= sorted_lst[i + 1]
@given(st.lists(st.integers()))
def test_reverse_twice_equals_original(lst):
"""Property: reversing twice returns original."""
assert list(reversed(list(reversed(lst)))) == lst
@given(st.lists(st.integers()), st.integers())
def test_append_then_pop_equals_original(lst, value):
"""Property: append + pop leaves list unchanged."""
original = lst.copy()
lst.append(value)
lst.pop()
assert lst == originalString Properties
Test string operations:
@given(st.text(), st.text())
def test_concatenation_length(s1, s2):
"""Property: length of concatenation equals sum of lengths."""
result = s1 + s2
assert len(result) == len(s1) + len(s2)
@given(st.text())
def test_lowercase_idempotent(s):
"""Property: lowercase twice equals lowercase once."""
assert s.lower().lower() == s.lower()
@given(st.text())
def test_strip_idempotent(s):
"""Property: strip twice equals strip once."""
assert s.strip().strip() == s.strip()Custom Strategies
Define domain-specific generators:
from hypothesis.strategies import composite
@composite
def valid_email(draw):
"""Generate valid email addresses."""
username = draw(st.text(alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Nd")), min_size=1))
domain = draw(st.text(alphabet=st.characters(whitelist_categories=("Lu", "Ll")), min_size=1))
tld = draw(st.sampled_from(["com", "org", "net", "edu"]))
return f"{username}@{domain}.{tld}"
@given(valid_email())
def test_email_parsing(email):
"""Test email validation with generated emails."""
assert "@" in email
assert "." in emailStateful Testing
Test sequences of operations:
from hypothesis.stateful import RuleBasedStateMachine, rule, initialize
class StackMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.stack = []
@rule(value=st.integers())
def push(self, value):
self.stack.append(value)
@rule()
def pop(self):
if self.stack:
self.stack.pop()
@rule()
def check_invariants(self):
# Stack is never negative length
assert len(self.stack) >= 0
TestStack = StackMachine.TestCaseConfiguration
Control test generation:
from hypothesis import given, settings, HealthCheck
@settings(max_examples=1000) # Run more examples
@given(st.integers())
def test_with_more_examples(x):
assert x == x
@settings(deadline=None) # No time limit per test
@given(st.lists(st.integers()))
def test_slow_operation(lst):
complex_operation(lst)
@settings(suppress_health_check=[HealthCheck.too_slow])
@given(st.text())
def test_slow_but_important(s):
expensive_operation(s)Use Cases
1. Universal properties: Test mathematical properties that should always hold 2. Edge case discovery: Find inputs you wouldn't think to test manually 3. Invariant validation: Verify data structures maintain invariants 4. Round-trip testing: Serialize then deserialize should equal original 5. Complement examples: Use with example-based tests for comprehensive coverage
Best Practices
1. Define clear properties: What should always be true? 2. Start simple: Basic properties before complex ones 3. Shrinking: Hypothesis finds minimal failing examples 4. Stateful for complex: Use stateful testing for sequences 5. Not a replacement: Complement example-based tests 6. Document properties: Explain why property should hold
Pytest Fundamentals
Basic Test Structure
Simple test with pytest:
# test_calculator.py
import pytest
class Calculator:
def add(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 basic addition."""
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
def test_division_by_zero():
"""Test exception handling."""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(5, 0)Key Concepts
- Test discovery: Files matching
test_*.pyor*_test.py - Test functions: Start with
test_ - Assertions: Use
assertstatements for verification - Exception testing:
pytest.raises()for exception testing - Running tests:
pytestorpytest -vfor verbose output
Command Line Options
# Run all tests
pytest
# Verbose output
pytest -v
# Run specific file
pytest tests/test_calculator.py
# Run specific test
pytest tests/test_calculator.py::test_addition
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run last failed tests
pytest --lfAAA Pattern
Arrange, Act, Assert:
def test_user_creation():
# Arrange
username = "testuser"
email = "test@example.com"
# Act
user = User(username=username, email=email)
# Assert
assert user.username == username
assert user.email == emailTest Organization and Structure
Directory Structure
Organize tests for maintainability:
project/
├── src/
│ ├── __init__.py
│ ├── models.py
│ ├── services.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── unit/ # Fast, isolated tests
│ │ ├── __init__.py
│ │ ├── test_models.py
│ │ ├── test_services.py
│ │ └── test_utils.py
│ ├── integration/ # Component interaction
│ │ ├── __init__.py
│ │ ├── test_api.py
│ │ └── test_database.py
│ └── e2e/ # End-to-end tests
│ ├── __init__.py
│ └── test_workflows.py
├── pytest.ini # Configuration
└── pyproject.toml # Project configTest Levels
Unit Tests
- Purpose: Test individual functions/classes in isolation
- Speed: Fast (<1ms per test)
- Dependencies: None (mocked)
- Location:
tests/unit/
Integration Tests
- Purpose: Test component interactions
- Speed: Medium (10-100ms per test)
- Dependencies: Real databases, APIs (local)
- Location:
tests/integration/
End-to-End Tests
- Purpose: Test complete user workflows
- Speed: Slow (100ms-1s+ per test)
- Dependencies: Full system stack
- Location:
tests/e2e/
conftest.py for Shared Fixtures
tests/conftest.py - Available to all tests:
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def database_url():
"""Provide test database URL."""
return "postgresql://localhost/test_db"
@pytest.fixture(scope="session")
def database_engine(database_url):
"""Create database engine."""
return create_engine(database_url)
@pytest.fixture
def database_session(database_engine):
"""Provide database session."""
Session = sessionmaker(bind=database_engine)
session = Session()
yield session
session.rollback()
session.close()
@pytest.fixture
def sample_user():
"""Sample user data for tests."""
return {
"id": 1,
"name": "Test User",
"email": "test@example.com"
}
@pytest.fixture(autouse=True)
def reset_state():
"""Auto-run cleanup before each test."""
# Setup
yield
# Teardown
passNested conftest.py
tests/integration/conftest.py - Only for integration tests:
import pytest
from myapp import create_app
@pytest.fixture
def app():
"""Create test app instance."""
app = create_app(testing=True)
yield app
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()Test Markers
Define in pytest.ini:
[pytest]
markers =
unit: Unit tests (fast, isolated)
integration: Integration tests (medium speed)
e2e: End-to-end tests (slow)
slow: Slow tests (skip for quick runs)
smoke: Smoke tests (critical functionality)
database: Tests requiring database
api: Tests requiring external APIUse in tests:
import pytest
@pytest.mark.unit
def test_pure_function():
"""Fast unit test."""
assert add(2, 3) == 5
@pytest.mark.integration
@pytest.mark.database
def test_database_operation(database_session):
"""Integration test with database."""
user = User(name="Test")
database_session.add(user)
database_session.commit()
assert user.id is not None
@pytest.mark.e2e
@pytest.mark.slow
def test_complete_workflow(client):
"""End-to-end workflow test."""
# Complex multi-step test
passTest Naming Conventions
Descriptive test names:
# Good: Clear what is being tested
def test_user_creation_with_valid_data():
pass
def test_email_validation_rejects_invalid_format():
pass
def test_api_returns_404_for_missing_user():
pass
# Bad: Unclear purpose
def test_user():
pass
def test_1():
passTest Classes
Group related tests:
class TestUserModel:
"""Tests for User model."""
def test_create_user(self):
user = User(name="Test")
assert user.name == "Test"
def test_user_validation(self):
with pytest.raises(ValueError):
User(name="")
class TestUserService:
"""Tests for User service."""
@pytest.fixture
def service(self):
return UserService()
def test_get_user(self, service):
user = service.get_user(1)
assert user is not NoneConfiguration Files
pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--tb=short
--cov=src
--cov-report=term-missing
--cov-report=html
markers =
unit: Unit tests
integration: Integration tests
e2e: End-to-end tests
slow: Slow testspyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--cov=src",
"--cov-report=term-missing",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/test_*.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]Running Tests by Category
# Run all tests
pytest
# Run only unit tests
pytest -m unit
# Run integration and e2e tests
pytest -m "integration or e2e"
# Run all except slow tests
pytest -m "not slow"
# Run specific directory
pytest tests/unit/
# Run specific file
pytest tests/unit/test_models.py
# Run specific test
pytest tests/unit/test_models.py::test_user_creationBest Practices
1. Parallel structure: Mirror source code organization 2. Clear separation: Unit/integration/e2e in separate directories 3. Shared fixtures: Use conftest.py for common setup 4. Descriptive names: Test names describe behavior 5. Appropriate markers: Tag tests for selective running 6. Fast by default: Unit tests run most frequently 7. Isolated tests: No dependencies between tests 8. Clean setup/teardown: Use fixtures for resource management
# Python Testing Patterns Skill Quality Rubric
version: "1.0.0"
skill_name: python-testing-patterns
evaluated_date: "2026-01-05"
dimensions:
clarity:
weight: 25
description: "Clear testing concepts and pytest usage"
criteria:
- "Test structure (AAA pattern) is clear"
- "Fixture usage explained progressively"
- "Mocking strategies are well-organized"
- "Parametrization syntax is clear"
completeness:
weight: 25
description: "Comprehensive Python testing coverage"
criteria:
- "Covers unit, integration, and e2e tests"
- "Includes pytest fixtures and markers"
- "Has mocking with unittest.mock"
- "Covers property-based testing (hypothesis)"
accuracy:
weight: 30
description: "Correct pytest patterns and practices"
criteria:
- "Fixture scopes are correctly explained"
- "Mock usage follows best practices"
- "Async testing is accurate"
- "Coverage configuration is correct"
usefulness:
weight: 20
description: "Practical Python testing guidance"
criteria:
- "Examples run with pytest directly"
- "Common testing patterns included"
- "CI/CD integration examples"
- "Debugging failed tests covered"
passing_criteria:
minimum_score: 3.5
target_score: 4.0
exceptional_score: 4.5
required_dimensions:
- accuracy
blocking_issues:
- "Incorrect fixture scope behavior"
- "Mock examples that don't patch correctly"
- "Tests that always pass (false positives)"
scoring_guide:
clarity:
"1": "Test concepts jumbled"
"2": "Confusing fixture examples"
"3": "Understandable basics"
"4": "Clear with runnable examples"
"5": "Exceptional pytest mastery"
accuracy:
"1": "Fundamentally wrong test patterns"
"2": "Mock/fixture errors"
"3": "Mostly correct pytest usage"
"4": "Follows pytest best practices"
"5": "Expert-level, comprehensive"