Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply Python testing best practices with pytest, fixtures, mocking, and coverage strategies.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/advanced-patterns.md
1# Python Testing Patterns — Advanced Reference23Advanced testing patterns including async code, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration.45## Pattern 6: Testing Async Code67```python8# test_async.py9import pytest10import asyncio1112async def fetch_data(url: str) -> dict:13"""Fetch data asynchronously."""14await asyncio.sleep(0.1)15return {"url": url, "data": "result"}161718@pytest.mark.asyncio19async def test_fetch_data():20"""Test async function."""21result = await fetch_data("https://api.example.com")22assert result["url"] == "https://api.example.com"23assert "data" in result242526@pytest.mark.asyncio27async def test_concurrent_fetches():28"""Test concurrent async operations."""29urls = ["url1", "url2", "url3"]30tasks = [fetch_data(url) for url in urls]31results = await asyncio.gather(*tasks)3233assert len(results) == 334assert all("data" in r for r in results)353637@pytest.fixture38async def async_client():39"""Async fixture."""40client = {"connected": True}41yield client42client["connected"] = False434445@pytest.mark.asyncio46async def test_with_async_fixture(async_client):47"""Test using async fixture."""48assert async_client["connected"] is True49```5051## Pattern 7: Monkeypatch for Testing5253```python54# test_environment.py55import os56import pytest5758def get_database_url() -> str:59"""Get database URL from environment."""60return os.environ.get("DATABASE_URL", "sqlite:///:memory:")616263def test_database_url_default():64"""Test default database URL."""65# Will use actual environment variable if set66url = get_database_url()67assert url686970def test_database_url_custom(monkeypatch):71"""Test custom database URL with monkeypatch."""72monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")73assert get_database_url() == "postgresql://localhost/test"747576def test_database_url_not_set(monkeypatch):77"""Test when env var is not set."""78monkeypatch.delenv("DATABASE_URL", raising=False)79assert get_database_url() == "sqlite:///:memory:"808182class Config:83"""Configuration class."""8485def __init__(self):86self.api_key = "production-key"8788def get_api_key(self):89return self.api_key909192def test_monkeypatch_attribute(monkeypatch):93"""Test monkeypatching object attributes."""94config = Config()95monkeypatch.setattr(config, "api_key", "test-key")96assert config.get_api_key() == "test-key"97```9899## Pattern 8: Temporary Files and Directories100101```python102# test_file_operations.py103import pytest104from pathlib import Path105106def save_data(filepath: Path, data: str):107"""Save data to file."""108filepath.write_text(data)109110111def load_data(filepath: Path) -> str:112"""Load data from file."""113return filepath.read_text()114115116def test_file_operations(tmp_path):117"""Test file operations with temporary directory."""118# tmp_path is a pathlib.Path object119test_file = tmp_path / "test_data.txt"120121# Save data122save_data(test_file, "Hello, World!")123124# Verify file exists125assert test_file.exists()126127# Load and verify data128data = load_data(test_file)129assert data == "Hello, World!"130131132def test_multiple_files(tmp_path):133"""Test with multiple temporary files."""134files = {135"file1.txt": "Content 1",136"file2.txt": "Content 2",137"file3.txt": "Content 3"138}139140for filename, content in files.items():141filepath = tmp_path / filename142save_data(filepath, content)143144# Verify all files created145assert len(list(tmp_path.iterdir())) == 3146147# Verify contents148for filename, expected_content in files.items():149filepath = tmp_path / filename150assert load_data(filepath) == expected_content151```152153## Pattern 9: Custom Fixtures and Conftest154155```python156# conftest.py157"""Shared fixtures for all tests."""158import pytest159160@pytest.fixture(scope="session")161def database_url():162"""Provide database URL for all tests."""163return "postgresql://localhost/test_db"164165166@pytest.fixture(autouse=True)167def reset_database(database_url):168"""Auto-use fixture that runs before each test."""169# Setup: Clear database170print(f"Clearing database: {database_url}")171yield172# Teardown: Clean up173print("Test completed")174175176@pytest.fixture177def sample_user():178"""Provide sample user data."""179return {180"id": 1,181"name": "Test User",182"email": "[email protected]"183}184185186@pytest.fixture187def sample_users():188"""Provide list of sample users."""189return [190{"id": 1, "name": "User 1"},191{"id": 2, "name": "User 2"},192{"id": 3, "name": "User 3"},193]194195196# Parametrized fixture197@pytest.fixture(params=["sqlite", "postgresql", "mysql"])198def db_backend(request):199"""Fixture that runs tests with different database backends."""200return request.param201202203def test_with_db_backend(db_backend):204"""This test will run 3 times with different backends."""205print(f"Testing with {db_backend}")206assert db_backend in ["sqlite", "postgresql", "mysql"]207```208209## Pattern 10: Property-Based Testing210211```python212# test_properties.py213from hypothesis import given, strategies as st214import pytest215216def reverse_string(s: str) -> str:217"""Reverse a string."""218return s[::-1]219220221@given(st.text())222def test_reverse_twice_is_original(s):223"""Property: reversing twice returns original."""224assert reverse_string(reverse_string(s)) == s225226227@given(st.text())228def test_reverse_length(s):229"""Property: reversed string has same length."""230assert len(reverse_string(s)) == len(s)231232233@given(st.integers(), st.integers())234def test_addition_commutative(a, b):235"""Property: addition is commutative."""236assert a + b == b + a237238239@given(st.lists(st.integers()))240def test_sorted_list_properties(lst):241"""Property: sorted list is ordered."""242sorted_lst = sorted(lst)243244# Same length245assert len(sorted_lst) == len(lst)246247# All elements present248assert set(sorted_lst) == set(lst)249250# Is ordered251for i in range(len(sorted_lst) - 1):252assert sorted_lst[i] <= sorted_lst[i + 1]253```254255## Testing Database Code256257```python258# test_database_models.py259import pytest260from sqlalchemy import create_engine, Column, Integer, String261from sqlalchemy.ext.declarative import declarative_base262from sqlalchemy.orm import sessionmaker, Session263264Base = declarative_base()265266267class User(Base):268"""User model."""269__tablename__ = "users"270271id = Column(Integer, primary_key=True)272name = Column(String(50))273email = Column(String(100), unique=True)274275276@pytest.fixture(scope="function")277def db_session() -> Session:278"""Create in-memory database for testing."""279engine = create_engine("sqlite:///:memory:")280Base.metadata.create_all(engine)281282SessionLocal = sessionmaker(bind=engine)283session = SessionLocal()284285yield session286287session.close()288289290def test_create_user(db_session):291"""Test creating a user."""292user = User(name="Test User", email="[email protected]")293db_session.add(user)294db_session.commit()295296assert user.id is not None297assert user.name == "Test User"298299300def test_query_user(db_session):301"""Test querying users."""302user1 = User(name="User 1", email="[email protected]")303user2 = User(name="User 2", email="[email protected]")304305db_session.add_all([user1, user2])306db_session.commit()307308users = db_session.query(User).all()309assert len(users) == 2310311312def test_unique_email_constraint(db_session):313"""Test unique email constraint."""314from sqlalchemy.exc import IntegrityError315316user1 = User(name="User 1", email="[email protected]")317user2 = User(name="User 2", email="[email protected]")318319db_session.add(user1)320db_session.commit()321322db_session.add(user2)323324with pytest.raises(IntegrityError):325db_session.commit()326```327328## CI/CD Integration329330```yaml331# .github/workflows/test.yml332name: Tests333334on: [push, pull_request]335336jobs:337test:338runs-on: ubuntu-latest339340strategy:341matrix:342python-version: ["3.9", "3.10", "3.11", "3.12"]343344steps:345- uses: actions/checkout@v3346347- name: Set up Python348uses: actions/setup-python@v4349with:350python-version: ${{ matrix.python-version }}351352- name: Install dependencies353run: |354pip install -e ".[dev]"355pip install pytest pytest-cov356357- name: Run tests358run: |359pytest --cov=myapp --cov-report=xml360361- name: Upload coverage362uses: codecov/codecov-action@v3363with:364file: ./coverage.xml365```366367## Configuration Files368369```ini370# pytest.ini371[pytest]372testpaths = tests373python_files = test_*.py374python_classes = Test*375python_functions = test_*376addopts =377-v378--strict-markers379--tb=short380--cov=myapp381--cov-report=term-missing382markers =383slow: marks tests as slow384integration: marks integration tests385unit: marks unit tests386e2e: marks end-to-end tests387```388389```toml390# pyproject.toml391[tool.pytest.ini_options]392testpaths = ["tests"]393python_files = ["test_*.py"]394addopts = [395"-v",396"--cov=myapp",397"--cov-report=term-missing",398]399400[tool.coverage.run]401source = ["myapp"]402omit = ["*/tests/*", "*/migrations/*"]403404[tool.coverage.report]405exclude_lines = [406"pragma: no cover",407"def __repr__",408"raise AssertionError",409"raise NotImplementedError",410]411```412