Claude Skill

python-pytest-ops

pytest testing patterns for Python. Triggers on: pytest, fixture, mark, parametrize, mock, conftest, test coverage, unit test, integration test, pytest.raises.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_python-pytest-ops-3dfaf0b.zip · 26 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-pytest-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Python pytest Patterns

Modern pytest patterns for effective testing.

Basic Test Structure

import pytest

def test_basic():
    """Simple assertion test."""
    assert 1 + 1 == 2

def test_with_description():
    """Descriptive name and docstring."""
    result = calculate_total([1, 2, 3])
    assert result == 6, "Sum should equal 6"

Fixtures

import pytest

@pytest.fixture
def sample_user():
    """Create test user."""
    return {"id": 1, "name": "Test User"}

@pytest.fixture
def db_connection():
    """Fixture with setup and teardown."""
    conn = create_connection()
    yield conn
    conn.close()

def test_user(sample_user):
    """Fixtures injected by name."""
    assert sample_user["name"] == "Test User"

Fixture Scopes

@pytest.fixture(scope="function")  # Default - per test
@pytest.fixture(scope="class")     # Per test class
@pytest.fixture(scope="module")    # Per test file
@pytest.fixture(scope="session")   # Entire test run

Parametrize

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    assert double(input) == expected

# Multiple parameters
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):  # 4 test combinations
    assert x * y > 0

Exception Testing

def test_raises():
    with pytest.raises(ValueError) as exc_info:
        raise ValueError("Invalid input")
    assert "Invalid" in str(exc_info.value)

def test_raises_match():
    with pytest.raises(ValueError, match=r".*[Ii]nvalid.*"):
        raise ValueError("Invalid input")

Markers

@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
    pass

@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_feature():
    pass

@pytest.mark.xfail(reason="Known bug")
def test_buggy():
    assert broken_function() == expected

@pytest.mark.slow
def test_performance():
    """Custom marker - register in pytest.ini."""
    pass

Mocking

from unittest.mock import Mock, patch, MagicMock

def test_with_mock():
    mock_api = Mock()
    mock_api.get.return_value = {"status": "ok"}
    result = mock_api.get("/endpoint")
    assert result["status"] == "ok"

@patch("module.external_api")
def test_with_patch(mock_api):
    mock_api.return_value = {"data": []}
    result = function_using_api()
    mock_api.assert_called_once()

pytest-mock (Recommended)

def test_with_mocker(mocker):
    mock_api = mocker.patch("module.api_call")
    mock_api.return_value = {"success": True}
    result = process_data()
    assert result["success"]

conftest.py

# tests/conftest.py - Shared fixtures

import pytest

@pytest.fixture(scope="session")
def app():
    """Application fixture available to all tests."""
    return create_app(testing=True)

@pytest.fixture
def client(app):
    """Test client fixture."""
    return app.test_client()

Quick Reference

Run these inside the project env — prefix with uv run (e.g. uv run pytest -v). Bare pytest is shown below for brevity.

Command Description
pytest Run all tests
pytest -v Verbose output
pytest -x Stop on first failure
pytest -k "test_name" Run matching tests
pytest -m slow Run marked tests
pytest --lf Rerun last failed
pytest --cov=src Coverage report
pytest -n auto Parallel (pytest-xdist)

Additional Resources

  • ./references/fixtures-advanced.md - Factory fixtures, autouse, conftest patterns
  • ./references/mocking-patterns.md - Mock, patch, MagicMock, side_effect
  • ./references/async-testing.md - pytest-asyncio patterns
  • ./references/coverage-strategies.md - pytest-cov, branch coverage, reports
  • ./references/integration-testing.md - Database fixtures, API testing, testcontainers
  • ./references/property-testing.md - Hypothesis framework, strategies, shrinking
  • ./references/test-architecture.md - Test pyramid, organization, isolation strategies

Scripts

  • ./scripts/run-tests.sh - Run tests with recommended options
  • ./scripts/generate-conftest.sh - Generate conftest.py boilerplate

Assets

  • ./assets/pytest.ini.template - Recommended pytest configuration
  • ./assets/conftest.py.template - Common fixture patterns

See Also

Related Skills:

  • python-typing-ops - Type-safe test code
  • python-async-ops - Async test patterns (pytest-asyncio)

Testing specific frameworks:

  • python-fastapi-ops - TestClient, API testing
  • python-database-ops - Database fixtures, transactions
Files (claude-mods)
  • assets
    • conftest.py.template 5.7 KB · in bundle
    • pytest.ini.template 996 B · in bundle
  • references
    • async-testing.md 6.6 KB
      # Async Testing Patterns
      
      Testing asyncio code with pytest-asyncio.
      
      ## Setup
      
      ```bash
      uv add --dev pytest-asyncio
      ```
      
      ```ini
      # pytest.ini or pyproject.toml
      [pytest]
      asyncio_mode = auto  # Recommended for pytest-asyncio 0.21+
      ```
      
      ## Basic Async Tests
      
      ```python
      import pytest
      
      @pytest.mark.asyncio
      async def test_async_function():
          result = await async_fetch_data()
          assert result["status"] == "ok"
      
      @pytest.mark.asyncio
      async def test_async_context_manager():
          async with AsyncResource() as resource:
              result = await resource.get()
              assert result is not None
      ```
      
      ## Async Fixtures
      
      ```python
      import pytest
      import aiohttp
      
      @pytest.fixture
      async def async_client():
          """Async fixture with cleanup."""
          async with aiohttp.ClientSession() as session:
              yield session
          # Session closed automatically
      
      @pytest.fixture
      async def database():
          """Async database fixture."""
          conn = await create_async_connection()
          await conn.execute("BEGIN")
          yield conn
          await conn.execute("ROLLBACK")
          await conn.close()
      
      @pytest.mark.asyncio
      async def test_with_async_fixture(async_client):
          async with async_client.get("https://httpbin.org/json") as resp:
              data = await resp.json()
              assert "slideshow" in data
      ```
      
      ## Fixture Scopes
      
      ```python
      @pytest.fixture(scope="session")
      async def app():
          """Session-scoped async fixture."""
          app = await create_app()
          yield app
          await app.shutdown()
      
      @pytest.fixture(scope="module")
      async def db_pool():
          """Module-scoped connection pool."""
          pool = await asyncpg.create_pool(DATABASE_URL)
          yield pool
          await pool.close()
      ```
      
      ## Testing Timeouts
      
      ```python
      import asyncio
      
      @pytest.mark.asyncio
      async def test_timeout():
          with pytest.raises(asyncio.TimeoutError):
              async with asyncio.timeout(0.1):
                  await asyncio.sleep(1.0)
      
      @pytest.mark.asyncio
      async def test_wait_for():
          with pytest.raises(asyncio.TimeoutError):
              await asyncio.wait_for(slow_operation(), timeout=0.1)
      ```
      
      ## Testing Cancellation
      
      ```python
      @pytest.mark.asyncio
      async def test_task_cancellation():
          task = asyncio.create_task(long_running_task())
          await asyncio.sleep(0.01)
          task.cancel()
      
          with pytest.raises(asyncio.CancelledError):
              await task
      
      @pytest.mark.asyncio
      async def test_graceful_cancellation():
          """Test that cleanup runs on cancellation."""
          cleanup_ran = False
      
          async def task_with_cleanup():
              nonlocal cleanup_ran
              try:
                  await asyncio.sleep(10)
              except asyncio.CancelledError:
                  cleanup_ran = True
                  raise
      
          task = asyncio.create_task(task_with_cleanup())
          await asyncio.sleep(0.01)
          task.cancel()
      
          with pytest.raises(asyncio.CancelledError):
              await task
      
          assert cleanup_ran
      ```
      
      ## Testing gather
      
      ```python
      @pytest.mark.asyncio
      async def test_gather_success():
          results = await asyncio.gather(
              async_op_1(),
              async_op_2(),
              async_op_3(),
          )
          assert len(results) == 3
      
      @pytest.mark.asyncio
      async def test_gather_with_exceptions():
          results = await asyncio.gather(
              async_op_1(),
              async_op_that_fails(),
              async_op_3(),
              return_exceptions=True
          )
          assert isinstance(results[1], Exception)
      ```
      
      ## Testing TaskGroup (Python 3.11+)
      
      ```python
      @pytest.mark.asyncio
      async def test_task_group():
          results = []
      
          async with asyncio.TaskGroup() as tg:
              tg.create_task(append_result(results, 1))
              tg.create_task(append_result(results, 2))
              tg.create_task(append_result(results, 3))
      
          assert sorted(results) == [1, 2, 3]
      
      @pytest.mark.asyncio
      async def test_task_group_exception():
          with pytest.raises(ExceptionGroup):
              async with asyncio.TaskGroup() as tg:
                  tg.create_task(successful_task())
                  tg.create_task(failing_task())
      ```
      
      ## Mocking Async Functions
      
      ```python
      from unittest.mock import AsyncMock
      
      @pytest.mark.asyncio
      async def test_mock_async_function(mocker):
          mock = mocker.patch("mymodule.async_api_call", new_callable=AsyncMock)
          mock.return_value = {"data": "mocked"}
      
          result = await mymodule.fetch_data()
      
          assert result == {"data": "mocked"}
          mock.assert_awaited_once()
      
      @pytest.mark.asyncio
      async def test_async_side_effect(mocker):
          mock = AsyncMock()
          mock.side_effect = [
              {"page": 1},
              {"page": 2},
              ValueError("No more pages"),
          ]
      
          assert await mock() == {"page": 1}
          assert await mock() == {"page": 2}
          with pytest.raises(ValueError):
              await mock()
      ```
      
      ## Testing aiohttp
      
      ```python
      import aiohttp
      from aiohttp import web
      import pytest
      
      @pytest.fixture
      async def app():
          """Create aiohttp app."""
          app = web.Application()
          app.router.add_get("/", home_handler)
          return app
      
      @pytest.fixture
      async def client(aiohttp_client, app):
          """Create test client."""
          return await aiohttp_client(app)
      
      @pytest.mark.asyncio
      async def test_endpoint(client):
          resp = await client.get("/")
          assert resp.status == 200
          data = await resp.json()
          assert "message" in data
      ```
      
      ## Testing WebSockets
      
      ```python
      @pytest.mark.asyncio
      async def test_websocket(aiohttp_client, app):
          client = await aiohttp_client(app)
      
          async with client.ws_connect("/ws") as ws:
              await ws.send_str("Hello")
              msg = await ws.receive()
              assert msg.type == aiohttp.WSMsgType.TEXT
              assert msg.data == "Hello back"
      ```
      
      ## Event Loop Fixtures
      
      ```python
      import pytest
      
      @pytest.fixture(scope="session")
      def event_loop_policy():
          """Custom event loop policy."""
          return asyncio.DefaultEventLoopPolicy()
      
      # For uvloop
      @pytest.fixture(scope="session")
      def event_loop_policy():
          import uvloop
          return uvloop.EventLoopPolicy()
      ```
      
      ## Testing Queues
      
      ```python
      @pytest.mark.asyncio
      async def test_queue_producer_consumer():
          queue = asyncio.Queue()
          results = []
      
          async def producer():
              for i in range(3):
                  await queue.put(i)
              await queue.put(None)  # Sentinel
      
          async def consumer():
              while True:
                  item = await queue.get()
                  if item is None:
                      break
                  results.append(item)
      
          await asyncio.gather(producer(), consumer())
          assert results == [0, 1, 2]
      ```
      
      ## Best Practices
      
      1. **Use `asyncio_mode = auto`** - Simplifies test marking
      2. **Scope fixtures appropriately** - Session for expensive resources
      3. **Use AsyncMock** - For mocking coroutines
      4. **Test cancellation** - Ensure cleanup happens
      5. **Test timeouts** - Verify timeout behavior
      6. **Avoid blocking calls** - Use `run_in_executor` if needed
      7. **Close resources** - Use async context managers
      
    • coverage-strategies.md 5.5 KB
      # Coverage Strategies
      
      Comprehensive code coverage with pytest-cov.
      
      ## Setup
      
      ```bash
      uv add --dev pytest-cov
      ```
      
      ## Basic Usage
      
      ```bash
      # Run with coverage
      pytest --cov=src
      
      # With terminal report
      pytest --cov=src --cov-report=term
      
      # With HTML report
      pytest --cov=src --cov-report=html
      open htmlcov/index.html
      
      # Multiple formats
      pytest --cov=src --cov-report=term --cov-report=html --cov-report=xml
      ```
      
      ## Coverage Configuration
      
      ### pyproject.toml
      
      ```toml
      [tool.coverage.run]
      source = ["src"]
      branch = true
      omit = [
          "*/tests/*",
          "*/__init__.py",
          "*/migrations/*",
      ]
      
      [tool.coverage.report]
      exclude_lines = [
          "pragma: no cover",
          "def __repr__",
          "raise NotImplementedError",
          "if TYPE_CHECKING:",
          "if __name__ == .__main__.:",
      ]
      fail_under = 80
      show_missing = true
      
      [tool.coverage.html]
      directory = "htmlcov"
      ```
      
      ### .coveragerc (Alternative)
      
      ```ini
      [run]
      source = src
      branch = true
      omit =
          */tests/*
          */__init__.py
      
      [report]
      exclude_lines =
          pragma: no cover
          raise NotImplementedError
      fail_under = 80
      
      [html]
      directory = htmlcov
      ```
      
      ## Branch Coverage
      
      ```python
      # branch=true catches this
      def process(value):
          if value > 0:
              return "positive"
          # Missing else branch without branch coverage
          return "non-positive"
      
      # Test both branches
      def test_positive():
          assert process(5) == "positive"
      
      def test_non_positive():
          assert process(-1) == "non-positive"
      ```
      
      ## Excluding Code
      
      ```python
      def debug_only():  # pragma: no cover
          """Never executed in production."""
          print("Debug info")
      
      if TYPE_CHECKING:  # Excluded by default config
          from typing import Optional
      
      def platform_specific():
          if sys.platform == "win32":  # pragma: no cover
              return windows_implementation()
          return unix_implementation()
      ```
      
      ## Coverage in CI
      
      ### GitHub Actions
      
      ```yaml
      name: Tests
      on: [push, pull_request]
      
      jobs:
        test:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - name: Install uv
              uses: astral-sh/setup-uv@v5
      
            - name: Install dependencies
              run: uv sync
      
            - name: Run tests with coverage
              run: uv run pytest --cov=src --cov-report=xml
      
            - name: Upload coverage to Codecov
              uses: codecov/codecov-action@v4
              with:
                files: ./coverage.xml
                fail_ci_if_error: true
      ```
      
      ### Fail on Low Coverage
      
      ```bash
      # Fail if coverage below 80%
      pytest --cov=src --cov-fail-under=80
      ```
      
      ## Measuring Coverage of Specific Tests
      
      ```bash
      # Coverage for specific test file
      pytest tests/test_api.py --cov=src/api
      
      # Coverage for marked tests only
      pytest -m "unit" --cov=src
      
      # Coverage for specific module
      pytest --cov=src/module_name
      ```
      
      ## Combining Coverage
      
      ```bash
      # Run tests in parallel, combine coverage
      pytest -n auto --cov=src --cov-append
      
      # Or manually combine
      coverage combine
      coverage report
      ```
      
      ## Coverage Diff (Incremental)
      
      ```bash
      # Show coverage for changed lines only (with diff-cover)
      uv add --dev diff-cover
      
      uv run pytest --cov=src --cov-report=xml
      uv run diff-cover coverage.xml --compare-branch=origin/main
      ```
      
      ## Mutation Testing
      
      ```bash
      # Beyond coverage: test quality with mutmut
      uv add --dev mutmut
      
      # Run mutation testing
      uv run mutmut run --paths-to-mutate=src/
      
      # View results
      uv run mutmut results
      uv run mutmut html
      ```
      
      ## Coverage Reports
      
      ### Terminal Report
      
      ```bash
      pytest --cov=src --cov-report=term-missing
      ```
      
      Output:
      ```
      Name                      Stmts   Miss Branch BrPart  Cover   Missing
      ---------------------------------------------------------------------
      src/api.py                   50      5     12      2    88%   45-49, 67
      src/utils.py                 30      0      8      0   100%
      ---------------------------------------------------------------------
      TOTAL                        80      5     20      2    92%
      ```
      
      ### HTML Report
      
      ```bash
      pytest --cov=src --cov-report=html
      # Creates htmlcov/index.html with line-by-line highlighting
      ```
      
      ### XML Report (CI)
      
      ```bash
      pytest --cov=src --cov-report=xml
      # Creates coverage.xml for CI tools
      ```
      
      ### JSON Report
      
      ```bash
      pytest --cov=src --cov-report=json
      # Creates coverage.json for programmatic access
      ```
      
      ## Coverage Best Practices
      
      ### 1. Aim for Meaningful Coverage
      
      ```python
      # BAD: 100% coverage but no assertions
      def test_function():
          result = my_function()  # Just call it
      
      # GOOD: Meaningful assertions
      def test_function():
          result = my_function()
          assert result.status == "success"
          assert len(result.items) > 0
      ```
      
      ### 2. Don't Chase 100%
      
      ```python
      # Some code genuinely shouldn't be tested
      def __repr__(self):  # pragma: no cover
          return f"<User {self.name}>"
      
      if __name__ == "__main__":  # pragma: no cover
          main()
      ```
      
      ### 3. Focus on Critical Paths
      
      ```python
      # Prioritize coverage for:
      # - Business logic
      # - Error handling
      # - Edge cases
      # - Security-sensitive code
      ```
      
      ### 4. Use Branch Coverage
      
      ```toml
      [tool.coverage.run]
      branch = true
      ```
      
      ### 5. Track Coverage Trends
      
      ```yaml
      # In CI: fail on coverage decrease
      - name: Check coverage
        run: |
          pytest --cov=src --cov-report=xml
          diff-cover coverage.xml --compare-branch=origin/main --fail-under=90
      ```
      
      ## Quick Reference
      
      | Command | Description |
      |---------|-------------|
      | `--cov=src` | Enable coverage for src/ |
      | `--cov-report=term` | Terminal report |
      | `--cov-report=html` | HTML report |
      | `--cov-report=xml` | XML report (CI) |
      | `--cov-fail-under=80` | Fail if under 80% |
      | `--cov-branch` | Enable branch coverage |
      | `--cov-append` | Append to existing data |
      | `--no-cov` | Disable coverage |
      
    • fixtures-advanced.md 5.2 KB
      # Advanced Fixture Patterns
      
      Deep dive into pytest fixtures for complex testing scenarios.
      
      ## Factory Fixtures
      
      ```python
      import pytest
      from dataclasses import dataclass
      
      @dataclass
      class User:
          id: int
          name: str
          email: str
      
      @pytest.fixture
      def user_factory():
          """Factory to create users with custom attributes."""
          def _create_user(
              id: int = 1,
              name: str = "Test User",
              email: str = "test@example.com"
          ) -> User:
              return User(id=id, name=name, email=email)
          return _create_user
      
      def test_user_factory(user_factory):
          user1 = user_factory()
          user2 = user_factory(id=2, name="Another User")
          assert user1.id != user2.id
      ```
      
      ## Fixture Dependencies
      
      ```python
      @pytest.fixture
      def database():
          """Base database fixture."""
          db = connect_to_test_db()
          yield db
          db.close()
      
      @pytest.fixture
      def clean_database(database):
          """Depends on database, adds cleanup."""
          database.clear_all()
          yield database
          database.clear_all()
      
      @pytest.fixture
      def seeded_database(clean_database):
          """Depends on clean_database, adds seed data."""
          clean_database.insert(SEED_DATA)
          return clean_database
      ```
      
      ## Autouse Fixtures
      
      ```python
      @pytest.fixture(autouse=True)
      def reset_environment():
          """Runs automatically before each test."""
          os.environ.clear()
          os.environ.update(TEST_ENV)
          yield
          os.environ.clear()
      
      @pytest.fixture(autouse=True, scope="module")
      def setup_logging():
          """Module-level autouse fixture."""
          logging.disable(logging.CRITICAL)
          yield
          logging.disable(logging.NOTSET)
      ```
      
      ## Request Fixture
      
      ```python
      @pytest.fixture
      def temp_file(request, tmp_path):
          """Fixture that adapts based on test parameters."""
          # Access test-specific data
          filename = getattr(request, "param", "default.txt")
          file_path = tmp_path / filename
          file_path.write_text("test content")
          return file_path
      
      @pytest.mark.parametrize("temp_file", ["custom.txt"], indirect=True)
      def test_with_custom_filename(temp_file):
          assert temp_file.name == "custom.txt"
      ```
      
      ## Fixture Finalization
      
      ```python
      @pytest.fixture
      def resource_with_finalizer(request):
          """Using request.addfinalizer for cleanup."""
          resource = allocate_resource()
      
          def cleanup():
              resource.release()
      
          request.addfinalizer(cleanup)
          return resource
      
      # Prefer yield-based cleanup when possible
      @pytest.fixture
      def resource_with_yield():
          """Preferred: yield-based cleanup."""
          resource = allocate_resource()
          yield resource
          resource.release()
      ```
      
      ## Fixture Caching
      
      ```python
      @pytest.fixture(scope="session")
      def expensive_computation():
          """Computed once, cached for entire session."""
          return perform_expensive_setup()
      
      @pytest.fixture(scope="module")
      def module_cache():
          """Cached per test module."""
          return load_module_data()
      ```
      
      ## Parametrized Fixtures
      
      ```python
      @pytest.fixture(params=["sqlite", "postgres", "mysql"])
      def database_backend(request):
          """Test runs 3 times, once per backend."""
          backend = request.param
          db = create_database(backend)
          yield db
          db.close()
      
      def test_database_operations(database_backend):
          """This test runs against all 3 databases."""
          database_backend.insert({"key": "value"})
          assert database_backend.get("key") == "value"
      ```
      
      ## Fixture with IDs
      
      ```python
      @pytest.fixture(
          params=[
              pytest.param({"user": "admin"}, id="admin-user"),
              pytest.param({"user": "guest"}, id="guest-user"),
          ]
      )
      def user_context(request):
          return request.param
      ```
      
      ## conftest.py Organization
      
      ```
      tests/
      ├── conftest.py              # Session/package-wide fixtures
      ├── unit/
      │   ├── conftest.py          # Unit test fixtures
      │   └── test_module.py
      ├── integration/
      │   ├── conftest.py          # Integration fixtures
      │   └── test_api.py
      └── e2e/
          ├── conftest.py          # E2E fixtures
          └── test_flows.py
      ```
      
      ### conftest.py Example
      
      ```python
      # tests/conftest.py
      import pytest
      
      def pytest_configure(config):
          """Called after command line parsing."""
          config.addinivalue_line("markers", "slow: marks slow tests")
      
      def pytest_collection_modifyitems(config, items):
          """Modify collected tests."""
          if config.getoption("--quick"):
              skip_slow = pytest.mark.skip(reason="--quick mode")
              for item in items:
                  if "slow" in item.keywords:
                      item.add_marker(skip_slow)
      
      @pytest.fixture(scope="session")
      def app():
          """Application for all tests."""
          from myapp import create_app
          return create_app(testing=True)
      
      @pytest.fixture
      def client(app):
          """Test client per test."""
          return app.test_client()
      
      @pytest.fixture
      def authenticated_client(client):
          """Client with auth token."""
          client.post("/login", json={"user": "test", "pass": "test"})
          return client
      ```
      
      ## Fixture Best Practices
      
      1. **Single responsibility** - Each fixture does one thing
      2. **Use factory fixtures** - When tests need variations
      3. **Scope appropriately** - Don't over-cache or under-cache
      4. **Prefer yield** - Over request.addfinalizer
      5. **Name clearly** - `db_connection` not `fixture1`
      6. **Document** - Explain what fixture provides and when to use
      7. **Minimize side effects** - Clean up after yourself
      
    • integration-testing.md 8.2 KB
      # Integration Testing Patterns
      
      Patterns for testing real systems, databases, and APIs.
      
      ## Database Testing with Transactions
      
      ```python
      import pytest
      from sqlalchemy import create_engine
      from sqlalchemy.orm import sessionmaker
      
      @pytest.fixture(scope="session")
      def engine():
          """Create test database engine."""
          engine = create_engine("postgresql://test:test@localhost/testdb")
          return engine
      
      @pytest.fixture(scope="session")
      def tables(engine):
          """Create all tables once per session."""
          Base.metadata.create_all(engine)
          yield
          Base.metadata.drop_all(engine)
      
      @pytest.fixture
      def db_session(engine, tables):
          """
          Transaction rollback fixture.
          Each test runs in a transaction that's rolled back.
          """
          connection = engine.connect()
          transaction = connection.begin()
          session = sessionmaker(bind=connection)()
      
          yield session
      
          session.close()
          transaction.rollback()
          connection.close()
      
      
      def test_user_creation(db_session):
          """Test runs in rolled-back transaction."""
          user = User(name="Test")
          db_session.add(user)
          db_session.commit()  # Committed to transaction, not DB
          assert db_session.query(User).count() == 1
          # Rolled back after test - no cleanup needed
      ```
      
      ## Async Database Testing
      
      ```python
      import pytest
      import pytest_asyncio
      from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
      
      @pytest_asyncio.fixture(scope="session")
      async def async_engine():
          engine = create_async_engine("postgresql+asyncpg://test:test@localhost/testdb")
          yield engine
          await engine.dispose()
      
      @pytest_asyncio.fixture
      async def async_session(async_engine):
          """Async session with rollback."""
          async with async_engine.connect() as conn:
              await conn.begin()
              async_session = AsyncSession(bind=conn)
      
              yield async_session
      
              await async_session.close()
              await conn.rollback()
      
      
      @pytest.mark.asyncio
      async def test_async_query(async_session):
          result = await async_session.execute(select(User))
          users = result.scalars().all()
          assert len(users) == 0
      ```
      
      ## TestContainers
      
      ```python
      # uv add --dev testcontainers
      
      import pytest
      from testcontainers.postgres import PostgresContainer
      from testcontainers.redis import RedisContainer
      
      @pytest.fixture(scope="session")
      def postgres():
          """Spin up PostgreSQL container for tests."""
          with PostgresContainer("postgres:15") as postgres:
              yield postgres
      
      @pytest.fixture(scope="session")
      def postgres_url(postgres):
          """Get connection URL for containerized PostgreSQL."""
          return postgres.get_connection_url()
      
      @pytest.fixture(scope="session")
      def redis():
          """Spin up Redis container for tests."""
          with RedisContainer("redis:7") as redis:
              yield redis
      
      @pytest.fixture
      def redis_client(redis):
          """Get Redis client for container."""
          import redis as redis_lib
          client = redis_lib.from_url(redis.get_container_host_ip())
          yield client
          client.flushdb()
      
      
      def test_with_real_postgres(postgres_url):
          """Test against real PostgreSQL container."""
          engine = create_engine(postgres_url)
          # Use real database
      ```
      
      ## FastAPI / Starlette Testing
      
      ```python
      import pytest
      from fastapi import FastAPI
      from fastapi.testclient import TestClient
      from httpx import AsyncClient
      
      # Synchronous testing
      @pytest.fixture
      def app():
          return create_app()
      
      @pytest.fixture
      def client(app):
          """Sync test client."""
          return TestClient(app)
      
      def test_endpoint(client):
          response = client.get("/api/users")
          assert response.status_code == 200
          assert "users" in response.json()
      
      
      # Async testing with httpx
      @pytest.fixture
      async def async_client(app):
          """Async test client for async endpoints."""
          async with AsyncClient(app=app, base_url="http://test") as client:
              yield client
      
      @pytest.mark.asyncio
      async def test_async_endpoint(async_client):
          response = await async_client.get("/api/users")
          assert response.status_code == 200
      
      
      # With database override
      @pytest.fixture
      def app_with_db(db_session):
          """Override database dependency."""
          app = create_app()
      
          def get_test_db():
              yield db_session
      
          app.dependency_overrides[get_db] = get_test_db
          yield app
          app.dependency_overrides.clear()
      ```
      
      ## API Testing Patterns
      
      ```python
      import pytest
      from dataclasses import dataclass
      
      @dataclass
      class APITestCase:
          """Structured API test case."""
          method: str
          path: str
          json: dict | None = None
          expected_status: int = 200
          expected_json: dict | None = None
          headers: dict | None = None
      
      @pytest.mark.parametrize("test_case", [
          APITestCase("GET", "/api/users", expected_status=200),
          APITestCase("POST", "/api/users", json={"name": "Test"}, expected_status=201),
          APITestCase("GET", "/api/users/999", expected_status=404),
      ])
      def test_api_endpoints(client, test_case):
          """Parametrized API testing."""
          response = client.request(
              method=test_case.method,
              url=test_case.path,
              json=test_case.json,
              headers=test_case.headers,
          )
          assert response.status_code == test_case.expected_status
      
          if test_case.expected_json:
              assert response.json() == test_case.expected_json
      
      
      # Request/Response validation
      def test_user_creation_flow(client):
          """Test complete user flow."""
          # Create
          response = client.post("/api/users", json={"name": "Test User"})
          assert response.status_code == 201
          user_id = response.json()["id"]
      
          # Read
          response = client.get(f"/api/users/{user_id}")
          assert response.status_code == 200
          assert response.json()["name"] == "Test User"
      
          # Update
          response = client.patch(f"/api/users/{user_id}", json={"name": "Updated"})
          assert response.status_code == 200
      
          # Delete
          response = client.delete(f"/api/users/{user_id}")
          assert response.status_code == 204
      ```
      
      ## Snapshot Testing
      
      ```python
      # uv add --dev syrupy
      
      import pytest
      from syrupy.assertion import SnapshotAssertion
      
      def test_api_response_snapshot(client, snapshot: SnapshotAssertion):
          """Compare response against stored snapshot."""
          response = client.get("/api/config")
          assert response.json() == snapshot
      
      
      def test_user_serialization(snapshot):
          """Snapshot complex objects."""
          user = User(id=1, name="Test", email="test@example.com")
          assert user.dict() == snapshot
      
      
      # Update snapshots: pytest --snapshot-update
      ```
      
      ## External Service Mocking
      
      ```python
      import pytest
      import responses
      import respx
      
      # responses (requests library)
      @responses.activate
      def test_external_api():
          responses.add(
              responses.GET,
              "https://api.example.com/data",
              json={"result": "mocked"},
              status=200
          )
      
          result = fetch_from_external_api()
          assert result["result"] == "mocked"
      
      
      # respx (httpx library)
      @pytest.fixture
      def mock_api():
          with respx.mock:
              yield respx
      
      def test_httpx_external(mock_api):
          mock_api.get("https://api.example.com/data").respond(
              json={"result": "mocked"}
          )
      
          result = fetch_with_httpx()
          assert result["result"] == "mocked"
      ```
      
      ## Factory Fixtures for Integration Tests
      
      ```python
      import pytest
      from faker import Faker
      
      fake = Faker()
      
      @pytest.fixture
      def user_factory(db_session):
          """Factory for creating test users."""
          created_users = []
      
          def _create_user(**kwargs):
              user = User(
                  name=kwargs.get("name", fake.name()),
                  email=kwargs.get("email", fake.email()),
                  **kwargs
              )
              db_session.add(user)
              db_session.commit()
              created_users.append(user)
              return user
      
          yield _create_user
      
          # Cleanup handled by transaction rollback
      
      
      def test_user_permissions(user_factory):
          admin = user_factory(role="admin")
          user = user_factory(role="user")
      
          assert admin.can_delete(user)
          assert not user.can_delete(admin)
      ```
      
      ## Quick Reference
      
      | Pattern | Use Case | Key Benefit |
      |---------|----------|-------------|
      | Transaction rollback | DB tests | Zero cleanup needed |
      | TestContainers | Real services | Production-like testing |
      | TestClient | API testing | Full HTTP stack |
      | Snapshot testing | Complex responses | Easy regression detection |
      | Factory fixtures | Data creation | Flexible test data |
      | respx/responses | External APIs | Isolated testing |
      
    • mocking-patterns.md 6.6 KB
      # Mocking Patterns
      
      Comprehensive guide to mocking in pytest.
      
      ## unittest.mock Basics
      
      ### Mock Object
      
      ```python
      from unittest.mock import Mock
      
      def test_mock_basics():
          mock = Mock()
      
          # Access any attribute (auto-created)
          mock.some_attribute
          mock.method()
          mock.nested.deeply.value
      
          # Configure return values
          mock.get_data.return_value = {"key": "value"}
          assert mock.get_data() == {"key": "value"}
      
          # Check calls
          mock.get_data.assert_called_once()
          mock.get_data.assert_called_with()  # No args
      ```
      
      ### MagicMock
      
      ```python
      from unittest.mock import MagicMock
      
      def test_magic_mock():
          mock = MagicMock()
      
          # Supports magic methods
          mock.__len__.return_value = 5
          assert len(mock) == 5
      
          # Iteration
          mock.__iter__.return_value = iter([1, 2, 3])
          assert list(mock) == [1, 2, 3]
      
          # Context manager
          mock.__enter__.return_value = "entered"
          with mock as m:
              assert m == "entered"
      ```
      
      ## patch Decorator
      
      ```python
      from unittest.mock import patch
      
      # Patch where used, not where defined
      @patch("mymodule.requests.get")
      def test_api_call(mock_get):
          mock_get.return_value.json.return_value = {"status": "ok"}
      
          result = mymodule.fetch_data()
      
          assert result["status"] == "ok"
          mock_get.assert_called_once_with("https://api.example.com/data")
      
      # Multiple patches (applied bottom-up)
      @patch("mymodule.save_to_db")
      @patch("mymodule.fetch_from_api")
      def test_multiple_patches(mock_fetch, mock_save):  # Note: reverse order
          mock_fetch.return_value = {"data": []}
          process_and_save()
          mock_save.assert_called_once()
      ```
      
      ## patch Context Manager
      
      ```python
      from unittest.mock import patch
      
      def test_with_context_manager():
          with patch("mymodule.external_service") as mock_service:
              mock_service.call.return_value = "mocked"
              result = mymodule.do_work()
              assert result == "mocked"
      
          # After context, original is restored
      ```
      
      ## patch.object
      
      ```python
      from unittest.mock import patch
      
      class MyClass:
          def method(self):
              return "real"
      
      def test_patch_object():
          obj = MyClass()
      
          with patch.object(obj, "method", return_value="mocked"):
              assert obj.method() == "mocked"
      
          assert obj.method() == "real"  # Restored
      ```
      
      ## patch.dict
      
      ```python
      from unittest.mock import patch
      import os
      
      def test_patch_dict():
          with patch.dict(os.environ, {"API_KEY": "test-key"}):
              assert os.environ["API_KEY"] == "test-key"
      
          # Clear and add
          with patch.dict(os.environ, {"NEW_VAR": "value"}, clear=True):
              assert "PATH" not in os.environ
              assert os.environ["NEW_VAR"] == "value"
      ```
      
      ## side_effect
      
      ```python
      from unittest.mock import Mock
      
      def test_side_effect_function():
          mock = Mock()
          mock.side_effect = lambda x: x * 2
          assert mock(5) == 10
      
      def test_side_effect_exception():
          mock = Mock()
          mock.side_effect = ValueError("Invalid input")
      
          with pytest.raises(ValueError):
              mock()
      
      def test_side_effect_list():
          mock = Mock()
          mock.side_effect = [1, 2, ValueError("Done")]
      
          assert mock() == 1
          assert mock() == 2
          with pytest.raises(ValueError):
              mock()
      ```
      
      ## spec and autospec
      
      ```python
      from unittest.mock import Mock, create_autospec
      
      class RealAPI:
          def get_user(self, user_id: int) -> dict:
              pass
      
          def create_user(self, name: str) -> dict:
              pass
      
      def test_with_spec():
          # Only allows methods that exist on RealAPI
          mock = Mock(spec=RealAPI)
          mock.get_user(1)  # OK
          # mock.invalid_method()  # AttributeError
      
      def test_with_autospec():
          # Also validates signatures
          mock = create_autospec(RealAPI)
          mock.get_user(1)  # OK
          # mock.get_user("string")  # Still OK at runtime, but IDE warns
          # mock.get_user(1, 2, 3)  # TypeError: too many args
      ```
      
      ## pytest-mock Plugin
      
      ```python
      # uv add --dev pytest-mock
      
      def test_with_mocker(mocker):
          # mocker is a fixture that wraps unittest.mock
          mock = mocker.patch("mymodule.external_call")
          mock.return_value = "mocked"
      
          result = mymodule.process()
      
          assert result == "mocked"
          mock.assert_called_once()
      
      def test_spy(mocker):
          # Spy: call real method but track calls
          spy = mocker.spy(mymodule, "helper_function")
      
          mymodule.main_function()
      
          spy.assert_called()
          # Original function was actually called
      
      def test_stub(mocker):
          # Stub: quick attribute replacement
          mocker.patch.object(MyClass, "expensive_method", return_value="cheap")
      ```
      
      ## Async Mocking
      
      ```python
      from unittest.mock import AsyncMock
      
      async def test_async_mock():
          mock = AsyncMock()
          mock.return_value = {"async": "result"}
      
          result = await mock()
      
          assert result == {"async": "result"}
          mock.assert_awaited_once()
      
      @patch("mymodule.async_fetch", new_callable=AsyncMock)
      async def test_patch_async(mock_fetch):
          mock_fetch.return_value = {"data": []}
      
          result = await mymodule.get_data()
      
          assert result == {"data": []}
      ```
      
      ## PropertyMock
      
      ```python
      from unittest.mock import PropertyMock, patch
      
      class MyClass:
          @property
          def value(self):
              return "real"
      
      def test_property_mock():
          with patch.object(
              MyClass, "value", new_callable=PropertyMock
          ) as mock_prop:
              mock_prop.return_value = "mocked"
              obj = MyClass()
              assert obj.value == "mocked"
      ```
      
      ## Common Patterns
      
      ### Mock HTTP Response
      
      ```python
      def test_mock_response(mocker):
          mock_response = Mock()
          mock_response.status_code = 200
          mock_response.json.return_value = {"id": 1}
          mock_response.raise_for_status = Mock()
      
          mocker.patch("requests.get", return_value=mock_response)
      
          result = fetch_user(1)
          assert result["id"] == 1
      ```
      
      ### Mock File Operations
      
      ```python
      from unittest.mock import mock_open, patch
      
      def test_file_read():
          m = mock_open(read_data="file content")
          with patch("builtins.open", m):
              result = read_config("config.txt")
              assert "content" in result
      
      def test_file_write():
          m = mock_open()
          with patch("builtins.open", m):
              write_data("output.txt", "data")
              m().write.assert_called_with("data")
      ```
      
      ### Mock datetime
      
      ```python
      from unittest.mock import patch
      from datetime import datetime
      
      def test_mock_datetime(mocker):
          mock_dt = mocker.patch("mymodule.datetime")
          mock_dt.now.return_value = datetime(2024, 1, 15, 12, 0, 0)
      
          result = mymodule.get_timestamp()
          assert "2024-01-15" in result
      ```
      
      ## Best Practices
      
      1. **Patch where used** - Not where defined
      2. **Use autospec** - Catch API mismatches
      3. **Reset mocks** - In fixtures or with `mock.reset_mock()`
      4. **Don't over-mock** - Test behavior, not implementation
      5. **Prefer dependency injection** - Over patching
      6. **Use pytest-mock** - Cleaner syntax than unittest.mock
      
    • property-testing.md 8.3 KB
      # Property-Based Testing with Hypothesis
      
      Test properties of code with generated inputs, not just examples.
      
      ## Why Property Testing?
      
      ```python
      # Example-based: tests specific cases
      def test_sort_examples():
          assert sort([3, 1, 2]) == [1, 2, 3]
          assert sort([]) == []
          assert sort([1]) == [1]
      
      # Property-based: tests properties for ANY input
      from hypothesis import given
      from hypothesis import strategies as st
      
      @given(st.lists(st.integers()))
      def test_sort_properties(lst):
          result = sort(lst)
          # Property 1: Same length
          assert len(result) == len(lst)
          # Property 2: Sorted order
          assert all(result[i] <= result[i+1] for i in range(len(result)-1))
          # Property 3: Same elements
          assert sorted(lst) == result
      ```
      
      ## Basic Hypothesis Usage
      
      ```python
      from hypothesis import given, settings, assume
      from hypothesis import strategies as st
      
      @given(st.integers(), st.integers())
      def test_addition_commutative(a, b):
          """Addition is commutative."""
          assert a + b == b + a
      
      @given(st.text())
      def test_reverse_twice(s):
          """Reversing twice returns original."""
          assert s[::-1][::-1] == s
      
      @given(st.lists(st.integers(), min_size=1))
      def test_max_in_list(lst):
          """Max is an element of the list."""
          assert max(lst) in lst
      ```
      
      ## Common Strategies
      
      ```python
      from hypothesis import strategies as st
      
      # Primitives
      st.integers()                    # Any integer
      st.integers(min_value=0)         # Non-negative
      st.floats(allow_nan=False)       # Floats without NaN
      st.text()                        # Unicode strings
      st.text(alphabet="abc", max_size=10)
      st.booleans()
      st.none()
      st.binary()                      # Bytes
      
      # Collections
      st.lists(st.integers())          # List of ints
      st.lists(st.text(), min_size=1, max_size=10)
      st.sets(st.integers())
      st.frozensets(st.text())
      st.dictionaries(st.text(), st.integers())
      
      # Tuples
      st.tuples(st.integers(), st.text())   # Fixed structure
      st.tuples(st.integers(), st.integers(), st.integers())
      
      # Optional / One of
      st.one_of(st.integers(), st.text())   # Either type
      st.none() | st.integers()              # Optional int
      st.sampled_from(["red", "green", "blue"])  # Enum-like
      ```
      
      ## Building Custom Strategies
      
      ```python
      from hypothesis import strategies as st
      from dataclasses import dataclass
      
      @dataclass
      class User:
          name: str
          age: int
          email: str
      
      # Strategy for User objects
      user_strategy = st.builds(
          User,
          name=st.text(min_size=1, max_size=50),
          age=st.integers(min_value=0, max_value=150),
          email=st.emails()
      )
      
      @given(user_strategy)
      def test_user_validation(user):
          assert validate_user(user)
      
      
      # Composite strategies for complex logic
      @st.composite
      def sorted_lists(draw):
          """Generate pre-sorted lists."""
          lst = draw(st.lists(st.integers()))
          return sorted(lst)
      
      @given(sorted_lists())
      def test_binary_search(sorted_lst):
          if sorted_lst:
              target = sorted_lst[len(sorted_lst) // 2]
              assert binary_search(sorted_lst, target) != -1
      
      
      # Dependent strategies
      @st.composite
      def list_and_index(draw):
          """Generate a list and valid index into it."""
          lst = draw(st.lists(st.integers(), min_size=1))
          index = draw(st.integers(min_value=0, max_value=len(lst)-1))
          return lst, index
      
      @given(list_and_index())
      def test_indexing(data):
          lst, index = data
          # This will never raise IndexError
          assert lst[index] is not None or lst[index] is None
      ```
      
      ## Filtering and Assumptions
      
      ```python
      from hypothesis import given, assume
      from hypothesis import strategies as st
      
      # Filter strategy (preferred when possible)
      @given(st.integers().filter(lambda x: x % 2 == 0))
      def test_even_numbers(n):
          assert n % 2 == 0
      
      # assume() for runtime filtering
      @given(st.integers(), st.integers())
      def test_division(a, b):
          assume(b != 0)  # Skip if b is 0
          assert (a // b) * b + (a % b) == a
      
      # Combining filters
      positive_even = st.integers(min_value=1).filter(lambda x: x % 2 == 0)
      ```
      
      ## Settings and Configuration
      
      ```python
      from hypothesis import given, settings, Verbosity, Phase
      from hypothesis import strategies as st
      
      # Per-test settings
      @settings(max_examples=500)  # More examples (default: 100)
      @given(st.integers())
      def test_thorough(n):
          pass
      
      @settings(deadline=None)  # Disable timing check
      @given(st.lists(st.integers()))
      def test_slow_operation(lst):
          expensive_operation(lst)
      
      @settings(
          max_examples=1000,
          verbosity=Verbosity.verbose,
          phases=[Phase.generate],  # Skip shrinking
      )
      @given(st.text())
      def test_verbose(s):
          pass
      
      
      # Profile for CI (in conftest.py)
      from hypothesis import settings, Verbosity
      
      settings.register_profile("ci", max_examples=1000)
      settings.register_profile("dev", max_examples=10)
      settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
      
      # Use: pytest --hypothesis-profile=ci
      ```
      
      ## Stateful Testing
      
      ```python
      from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
      from hypothesis import strategies as st
      
      class DatabaseMachine(RuleBasedStateMachine):
          """Test database operations maintain invariants."""
      
          def __init__(self):
              super().__init__()
              self.db = {}  # Model
              self.real_db = RealDatabase()  # System under test
      
          @rule(key=st.text(), value=st.integers())
          def set_value(self, key, value):
              """Set a value in both model and real DB."""
              self.db[key] = value
              self.real_db.set(key, value)
      
          @rule(key=st.text())
          def get_value(self, key):
              """Get value should match model."""
              expected = self.db.get(key)
              actual = self.real_db.get(key)
              assert expected == actual
      
          @rule(key=st.text())
          def delete_value(self, key):
              """Delete from both."""
              self.db.pop(key, None)
              self.real_db.delete(key)
      
          @invariant()
          def keys_match(self):
              """Keys should always match."""
              assert set(self.db.keys()) == set(self.real_db.keys())
      
      
      # Run stateful tests
      TestDatabase = DatabaseMachine.TestCase
      ```
      
      ## pytest Integration
      
      ```python
      # conftest.py
      from hypothesis import settings, Verbosity, Phase
      
      # Default profile for all tests
      settings.register_profile("default", max_examples=100)
      
      # CI profile - more examples, deterministic
      settings.register_profile(
          "ci",
          max_examples=500,
          derandomize=True,  # Deterministic for CI
      )
      
      # Load profile from env or default
      import os
      settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default"))
      
      
      # pytest.ini
      # [pytest]
      # addopts = --hypothesis-profile=default
      ```
      
      ## Shrinking Examples
      
      ```python
      from hypothesis import given, settings
      from hypothesis import strategies as st
      
      @given(st.lists(st.integers()))
      def test_shrinking_demo(lst):
          """Hypothesis shrinks failing inputs to minimal examples."""
          # This will fail, but Hypothesis finds minimal case
          assert sum(lst) < 100
      
      # Hypothesis will shrink to something like:
      # Falsifying example: test_shrinking_demo(lst=[100])
      # Not: test_shrinking_demo(lst=[3847, -293, 10293, ...])
      ```
      
      ## Common Patterns
      
      ```python
      # Roundtrip / Encode-Decode
      @given(st.binary())
      def test_compression_roundtrip(data):
          assert decompress(compress(data)) == data
      
      @given(st.dictionaries(st.text(), st.integers()))
      def test_json_roundtrip(d):
          assert json.loads(json.dumps(d)) == d
      
      
      # Oracle testing (compare implementations)
      @given(st.lists(st.integers()))
      def test_sort_vs_stdlib(lst):
          assert my_sort(lst) == sorted(lst)
      
      
      # Metamorphic relations
      @given(st.lists(st.integers()))
      def test_sort_idempotent(lst):
          """Sorting twice equals sorting once."""
          assert sort(sort(lst)) == sort(lst)
      
      @given(st.lists(st.integers()), st.integers())
      def test_sort_append(lst, x):
          """Appending and sorting vs inserting sorted."""
          assert sort(lst + [x]) == sort(sorted(lst) + [x])
      ```
      
      ## Quick Reference
      
      | Strategy | Description |
      |----------|-------------|
      | `st.integers()` | Any integer |
      | `st.floats()` | Floats (configure nan, inf) |
      | `st.text()` | Unicode strings |
      | `st.binary()` | Byte strings |
      | `st.lists(st.X())` | Lists of X |
      | `st.dictionaries(k, v)` | Dict with key/value strategies |
      | `st.builds(Class, ...)` | Build objects |
      | `st.one_of(a, b)` | Either a or b |
      | `st.sampled_from([...])` | Pick from list |
      | `@st.composite` | Custom strategy |
      
      | Setting | Purpose |
      |---------|---------|
      | `max_examples=N` | Number of test cases |
      | `deadline=None` | Disable timing |
      | `derandomize=True` | Reproducible runs |
      | `verbosity=Verbosity.verbose` | Debug output |
      
    • test-architecture.md 8.7 KB
      # Test Architecture Patterns
      
      Organize tests for maintainability, speed, and confidence.
      
      ## Test Pyramid
      
      ```
                       ┌─────────────┐
                       │     E2E     │  Few, slow, high confidence
                       │   Browser   │
                       ├─────────────┤
                       │ Integration │  Moderate, real services
                       │   API/DB    │
                       ├─────────────┤
                       │    Unit     │  Many, fast, isolated
                       │  Functions  │
                       └─────────────┘
      ```
      
      | Layer | Count | Speed | Scope | Tools |
      |-------|-------|-------|-------|-------|
      | Unit | 70% | <1ms | Single function | pytest, mock |
      | Integration | 20% | <1s | Multiple components | testcontainers, FastAPI TestClient |
      | E2E | 10% | <30s | Full system | Playwright, Selenium |
      
      ## Directory Structure
      
      ```
      project/
      ├── src/
      │   └── myapp/
      │       ├── models/
      │       ├── services/
      │       └── api/
      ├── tests/
      │   ├── conftest.py           # Shared fixtures
      │   ├── unit/                  # Fast, isolated tests
      │   │   ├── conftest.py
      │   │   ├── test_models.py
      │   │   └── test_services.py
      │   ├── integration/           # Real services tests
      │   │   ├── conftest.py        # DB, Redis fixtures
      │   │   ├── test_api.py
      │   │   └── test_repositories.py
      │   ├── e2e/                   # End-to-end tests
      │   │   ├── conftest.py
      │   │   └── test_user_flows.py
      │   └── fixtures/              # Shared test data
      │       └── users.json
      └── pytest.ini
      ```
      
      ## pytest Configuration
      
      ```ini
      # pytest.ini
      [pytest]
      testpaths = tests
      python_files = test_*.py
      python_functions = test_*
      python_classes = Test*
      
      # Markers for test categories
      markers =
          unit: Unit tests (fast, isolated)
          integration: Integration tests (requires services)
          e2e: End-to-end tests (full system)
          slow: Slow tests (>1s)
      
      # Default options
      addopts =
          -ra                 # Show summary of all except passed
          --strict-markers    # Error on unknown markers
          -q                  # Quiet mode
      ```
      
      ## Test Isolation Strategies
      
      ### 1. Database Isolation with Transactions
      
      ```python
      @pytest.fixture
      def db_session(engine):
          """Each test runs in a rolled-back transaction."""
          connection = engine.connect()
          transaction = connection.begin()
          session = Session(bind=connection)
      
          yield session
      
          session.close()
          transaction.rollback()
          connection.close()
      ```
      
      ### 2. Schema Isolation (Parallel Safe)
      
      ```python
      import uuid
      
      @pytest.fixture(scope="session")
      def test_schema(engine):
          """Create isolated schema for test session."""
          schema_name = f"test_{uuid.uuid4().hex[:8]}"
      
          with engine.connect() as conn:
              conn.execute(f"CREATE SCHEMA {schema_name}")
              conn.execute(f"SET search_path TO {schema_name}")
      
          yield schema_name
      
          with engine.connect() as conn:
              conn.execute(f"DROP SCHEMA {schema_name} CASCADE")
      ```
      
      ### 3. Container Isolation
      
      ```python
      @pytest.fixture(scope="session")
      def isolated_postgres():
          """Each test session gets fresh PostgreSQL."""
          with PostgresContainer("postgres:15") as pg:
              yield pg.get_connection_url()
      ```
      
      ## conftest.py Patterns
      
      ### Root conftest.py
      
      ```python
      # tests/conftest.py
      import pytest
      from typing import Generator
      
      # Session-scoped fixtures
      @pytest.fixture(scope="session")
      def app():
          """Create application once per session."""
          from myapp import create_app
          return create_app(testing=True)
      
      @pytest.fixture(scope="session")
      def engine(app):
          """Database engine for session."""
          return app.extensions["db"].engine
      
      # Function-scoped (per-test)
      @pytest.fixture
      def client(app) -> Generator:
          """Test client per test."""
          with app.test_client() as client:
              yield client
      ```
      
      ### Unit Test conftest.py
      
      ```python
      # tests/unit/conftest.py
      import pytest
      from unittest.mock import Mock
      
      @pytest.fixture
      def mock_db():
          """Mock database for unit tests."""
          return Mock()
      
      @pytest.fixture
      def mock_redis():
          """Mock Redis for unit tests."""
          return Mock()
      
      @pytest.fixture(autouse=True)
      def no_network(monkeypatch):
          """Prevent network calls in unit tests."""
          import socket
          monkeypatch.setattr(socket, "socket", Mock(side_effect=Exception("No network in unit tests!")))
      ```
      
      ### Integration Test conftest.py
      
      ```python
      # tests/integration/conftest.py
      import pytest
      
      @pytest.fixture(scope="session")
      def postgres_container():
          """PostgreSQL container for integration tests."""
          from testcontainers.postgres import PostgresContainer
          with PostgresContainer("postgres:15") as pg:
              yield pg
      
      @pytest.fixture
      def db_session(postgres_container):
          """Database session with rollback."""
          # Transaction rollback pattern
          ...
      ```
      
      ## Test Markers and Selection
      
      ```python
      import pytest
      
      # Mark tests by category
      @pytest.mark.unit
      def test_calculate_total():
          assert calculate_total([1, 2, 3]) == 6
      
      @pytest.mark.integration
      def test_save_to_database(db_session):
          user = User(name="Test")
          db_session.add(user)
          db_session.commit()
          assert user.id is not None
      
      @pytest.mark.e2e
      def test_user_signup_flow(browser):
          browser.goto("/signup")
          browser.fill("email", "test@example.com")
          browser.click("Submit")
          assert browser.url == "/dashboard"
      
      @pytest.mark.slow
      def test_data_migration():
          migrate_all_records()  # Takes 30 seconds
      ```
      
      ```bash
      # Run specific categories
      pytest -m unit            # Only unit tests
      pytest -m integration     # Only integration tests
      pytest -m "not slow"      # Exclude slow tests
      pytest -m "unit or integration"  # Both
      ```
      
      ## Parallel Testing
      
      ```python
      # pytest.ini
      [pytest]
      # Safe for parallel execution
      addopts = -n auto  # Use pytest-xdist
      
      # conftest.py - ensure isolation
      @pytest.fixture(scope="session")
      def worker_id(request):
          """Get unique worker ID for parallel runs."""
          if hasattr(request.config, "workerinput"):
              return request.config.workerinput["workerid"]
          return "master"
      
      @pytest.fixture(scope="session")
      def db_name(worker_id):
          """Unique database per worker."""
          return f"testdb_{worker_id}"
      ```
      
      ## Test Naming Conventions
      
      ```python
      # Pattern: test_<unit>_<condition>_<expected>
      
      def test_user_creation_with_valid_data_succeeds():
          pass
      
      def test_user_creation_with_missing_email_raises_validation_error():
          pass
      
      def test_calculate_total_with_empty_list_returns_zero():
          pass
      
      def test_api_users_get_without_auth_returns_401():
          pass
      
      
      # Or use classes for grouping
      class TestUserCreation:
          def test_with_valid_data_succeeds(self):
              pass
      
          def test_with_missing_email_raises_validation_error(self):
              pass
      
          def test_with_duplicate_email_raises_conflict_error(self):
              pass
      ```
      
      ## Fixture Organization
      
      ```python
      # tests/fixtures/factories.py
      import factory
      from faker import Faker
      
      fake = Faker()
      
      class UserFactory(factory.Factory):
          class Meta:
              model = User
      
          name = factory.LazyAttribute(lambda _: fake.name())
          email = factory.LazyAttribute(lambda _: fake.email())
      
      class OrderFactory(factory.Factory):
          class Meta:
              model = Order
      
          user = factory.SubFactory(UserFactory)
          total = factory.LazyAttribute(lambda _: fake.pydecimal(min_value=1, max_value=1000))
      
      
      # tests/conftest.py
      from tests.fixtures.factories import UserFactory, OrderFactory
      
      @pytest.fixture
      def user():
          return UserFactory()
      
      @pytest.fixture
      def order(user):
          return OrderFactory(user=user)
      ```
      
      ## Performance Testing
      
      ```python
      # uv add --dev pytest-benchmark
      
      def test_sort_performance(benchmark):
          """Benchmark sorting algorithm."""
          data = list(range(10000, 0, -1))
          result = benchmark(sort, data)
          assert result == sorted(data)
      
      
      # uv add --dev pytest-timeout
      @pytest.mark.timeout(5)  # Fail if takes >5 seconds
      def test_with_timeout():
          slow_operation()
      
      
      # Track memory
      # uv add --dev pytest-memray
      @pytest.mark.limit_memory("100 MB")
      def test_memory_usage():
          large_operation()
      ```
      
      ## Quick Reference
      
      | Pattern | When to Use |
      |---------|-------------|
      | Transaction rollback | Database tests, fast isolation |
      | TestContainers | Real service behavior needed |
      | Schema isolation | Parallel test execution |
      | Factory fixtures | Complex test data |
      | Markers | Categorize and filter tests |
      | conftest layers | Scope fixtures appropriately |
      
      | Command | Purpose |
      |---------|---------|
      | `pytest -m unit` | Run unit tests only |
      | `pytest -n auto` | Parallel execution |
      | `pytest --lf` | Last failed only |
      | `pytest -x` | Stop on first failure |
      | `pytest --cov=src` | Coverage report |
      
  • scripts
    • generate-conftest.sh 6.4 KB
      #!/bin/bash
      # Generate a pytest conftest.py with optional async/db/api fixture blocks.
      #
      # Usage:   generate-conftest.sh [--async] [--db] [--api]
      # Input:   optional flags selecting fixture blocks; writes ./tests/conftest.py
      # Output:  the generated conftest.py file plus a one-line summary on stdout
      # Stderr:  none (the overwrite confirmation prompt is read from the tty)
      # Exit:    0 ok (also --help, and when an existing file is kept at the prompt)
      #
      # Examples:
      #   generate-conftest.sh
      #   generate-conftest.sh --async --db
      #   generate-conftest.sh --api
      
      set -e
      
      # --help / -h: print usage to stdout and exit 0 before any side effects (so the
      # prompt never fires and no file is touched). Behavior-preserving for all other
      # invocations — only -h/--help short-circuits here.
      for _arg in "$@"; do
        case "$_arg" in
          -h|--help)
            cat <<'EOF'
      Usage: generate-conftest.sh [--async] [--db] [--api]
      
      Generate ./tests/conftest.py with optional fixture blocks. If a conftest.py
      already exists, it prompts for confirmation (default: keep the existing file).
      
      Options:
        --async   include async fixtures (event_loop, async_client)
        --db      include database fixtures (db_engine, db_session)
        --api     include API test fixtures (app, client, authenticated_client)
      
      Examples:
        generate-conftest.sh
        generate-conftest.sh --async --db
        generate-conftest.sh --api
      EOF
            exit 0
            ;;
        esac
      done
      
      OUTPUT="tests/conftest.py"
      
      # Check if tests directory exists
      if [[ ! -d "tests" ]]; then
          echo "Creating tests directory..."
          mkdir -p tests
      fi
      
      # Check if conftest.py already exists
      if [[ -f "$OUTPUT" ]]; then
          read -p "conftest.py already exists. Overwrite? [y/N] " -n 1 -r
          echo
          if [[ ! $REPLY =~ ^[Yy]$ ]]; then
              exit 0
          fi
      fi
      
      # Parse arguments
      ASYNC=""
      DB=""
      API=""
      
      while [[ $# -gt 0 ]]; do
          case $1 in
              --async)
                  ASYNC=1
                  shift
                  ;;
              --db)
                  DB=1
                  shift
                  ;;
              --api)
                  API=1
                  shift
                  ;;
              *)
                  shift
                  ;;
          esac
      done
      
      # Generate conftest.py
      cat > "$OUTPUT" << 'HEADER'
      """
      Pytest configuration and fixtures.
      Generated by generate-conftest.sh
      """
      import pytest
      HEADER
      
      # Add async imports if needed
      if [[ -n "$ASYNC" ]]; then
          cat >> "$OUTPUT" << 'ASYNC_IMPORTS'
      import asyncio
      ASYNC_IMPORTS
      fi
      
      # Add database imports if needed
      if [[ -n "$DB" ]]; then
          cat >> "$OUTPUT" << 'DB_IMPORTS'
      from sqlalchemy import create_engine
      from sqlalchemy.orm import sessionmaker
      DB_IMPORTS
      fi
      
      # Add API imports if needed
      if [[ -n "$API" ]]; then
          cat >> "$OUTPUT" << 'API_IMPORTS'
      from fastapi.testclient import TestClient
      # or: from flask.testing import FlaskClient
      API_IMPORTS
      fi
      
      # Add base fixtures
      cat >> "$OUTPUT" << 'BASE_FIXTURES'
      
      
      # ============================================================
      # Test Configuration
      # ============================================================
      
      def pytest_configure(config):
          """Register custom markers."""
          config.addinivalue_line("markers", "slow: marks tests as slow")
          config.addinivalue_line("markers", "integration: marks integration tests")
          config.addinivalue_line("markers", "e2e: marks end-to-end tests")
      
      
      def pytest_collection_modifyitems(config, items):
          """Skip slow tests unless --slow flag is provided."""
          if not config.getoption("--slow", default=False):
              skip_slow = pytest.mark.skip(reason="use --slow to run")
              for item in items:
                  if "slow" in item.keywords:
                      item.add_marker(skip_slow)
      
      
      def pytest_addoption(parser):
          """Add custom CLI options."""
          parser.addoption(
              "--slow",
              action="store_true",
              default=False,
              help="run slow tests"
          )
      
      
      # ============================================================
      # Common Fixtures
      # ============================================================
      
      @pytest.fixture
      def sample_data():
          """Provide sample test data."""
          return {
              "id": 1,
              "name": "Test",
              "active": True,
          }
      
      
      @pytest.fixture
      def temp_file(tmp_path):
          """Create a temporary file for testing."""
          file_path = tmp_path / "test_file.txt"
          file_path.write_text("test content")
          return file_path
      BASE_FIXTURES
      
      # Add async fixtures if requested
      if [[ -n "$ASYNC" ]]; then
          cat >> "$OUTPUT" << 'ASYNC_FIXTURES'
      
      
      # ============================================================
      # Async Fixtures
      # ============================================================
      
      @pytest.fixture(scope="session")
      def event_loop():
          """Create event loop for async tests."""
          loop = asyncio.new_event_loop()
          yield loop
          loop.close()
      
      
      @pytest.fixture
      async def async_client():
          """Async HTTP client fixture."""
          import aiohttp
          async with aiohttp.ClientSession() as session:
              yield session
      ASYNC_FIXTURES
      fi
      
      # Add database fixtures if requested
      if [[ -n "$DB" ]]; then
          cat >> "$OUTPUT" << 'DB_FIXTURES'
      
      
      # ============================================================
      # Database Fixtures
      # ============================================================
      
      @pytest.fixture(scope="session")
      def db_engine():
          """Create test database engine."""
          engine = create_engine("sqlite:///:memory:")
          # Create tables here
          yield engine
          engine.dispose()
      
      
      @pytest.fixture
      def db_session(db_engine):
          """Create database session with transaction rollback."""
          Session = sessionmaker(bind=db_engine)
          session = Session()
          yield session
          session.rollback()
          session.close()
      DB_FIXTURES
      fi
      
      # Add API fixtures if requested
      if [[ -n "$API" ]]; then
          cat >> "$OUTPUT" << 'API_FIXTURES'
      
      
      # ============================================================
      # API Fixtures
      # ============================================================
      
      @pytest.fixture
      def app():
          """Create test application."""
          from myapp import create_app
          app = create_app(testing=True)
          return app
      
      
      @pytest.fixture
      def client(app):
          """Create test client."""
          return TestClient(app)
      
      
      @pytest.fixture
      def authenticated_client(client):
          """Create authenticated test client."""
          # Add authentication logic here
          client.headers["Authorization"] = "Bearer test-token"
          return client
      API_FIXTURES
      fi
      
      echo "Generated $OUTPUT"
      echo ""
      echo "Options used:"
      [[ -n "$ASYNC" ]] && echo "  --async: Async fixtures included"
      [[ -n "$DB" ]] && echo "  --db: Database fixtures included"
      [[ -n "$API" ]] && echo "  --api: API fixtures included"
      
      exit 0
      
    • run-tests.sh 2 KB
      #!/bin/bash
      # Run pytest with recommended options
      # Usage: ./run-tests.sh [options]
      #
      # Options:
      #   --quick     Skip slow tests, minimal output
      #   --coverage  Run with coverage report
      #   --watch     Watch mode with pytest-watch
      #   --failed    Re-run only failed tests
      #   --debug     Enable debug output
      
      set -e
      
      # Colors
      RED='\033[0;31m'
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      NC='\033[0m'
      
      # Default options
      PYTEST_ARGS="-v"
      COVERAGE=""
      WATCH=""
      
      # Parse arguments
      while [[ $# -gt 0 ]]; do
          case $1 in
              --quick)
                  PYTEST_ARGS="-q -x --tb=short"
                  shift
                  ;;
              --coverage)
                  COVERAGE="--cov=src --cov-report=term-missing --cov-report=html"
                  shift
                  ;;
              --watch)
                  WATCH=1
                  shift
                  ;;
              --failed)
                  PYTEST_ARGS="$PYTEST_ARGS --lf"
                  shift
                  ;;
              --debug)
                  PYTEST_ARGS="$PYTEST_ARGS -s --tb=long"
                  shift
                  ;;
              *)
                  PYTEST_ARGS="$PYTEST_ARGS $1"
                  shift
                  ;;
          esac
      done
      
      # Check if pytest is installed
      if ! command -v pytest &> /dev/null; then
          echo -e "${RED}pytest not found. Install with: uv add --dev pytest${NC}"
          exit 1
      fi
      
      # Watch mode
      if [[ -n "$WATCH" ]]; then
          if ! command -v ptw &> /dev/null; then
              echo -e "${YELLOW}pytest-watch not found. Install with: uv add --dev pytest-watch${NC}"
              exit 1
          fi
          echo -e "${GREEN}Starting watch mode...${NC}"
          ptw -- $PYTEST_ARGS $COVERAGE
          exit 0
      fi
      
      # Run tests
      echo -e "${GREEN}Running pytest...${NC}"
      echo "pytest $PYTEST_ARGS $COVERAGE"
      echo ""
      
      pytest $PYTEST_ARGS $COVERAGE
      
      # Open coverage report if generated
      if [[ -n "$COVERAGE" ]] && [[ -f "htmlcov/index.html" ]]; then
          echo ""
          echo -e "${GREEN}Coverage report: htmlcov/index.html${NC}"
          if command -v open &> /dev/null; then
              read -p "Open coverage report? [y/N] " -n 1 -r
              echo
              if [[ $REPLY =~ ^[Yy]$ ]]; then
                  open htmlcov/index.html
              fi
          fi
      fi
      
  • tests
    • run.sh 6.1 KB
      #!/usr/bin/env bash
      # Self-test for python-pytest-ops — fully offline, deterministic, Linux-safe.
      #
      # generate-conftest.sh writes ./tests/conftest.py relative to the cwd and, when
      # one already exists, prompts interactively (defaulting to KEEP the file). Every
      # run executes inside a mktemp -d sandbox so nothing is ever written into the
      # repo or a real project. Asserts the --help contract, the generated conftest's
      # fixture blocks (base + --async/--db/--api), and the overwrite-safety guarantee:
      # an existing user conftest.py is NOT clobbered unless the user answers the
      # prompt with "y" (the documented mechanism — fed "n" here keeps the file).
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures
      
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      V="$SKILL/scripts/generate-conftest.sh"
      
      SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; }
      expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
      expect_has()  { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
      
      echo "=== python-pytest-ops self-test ==="
      
      # ── contract ──────────────────────────────────────────────────────────────────
      echo "-- contract --"
      bash -n "$V" 2>/dev/null && ok "bash -n generate-conftest.sh" || no "bash -n generate-conftest.sh"
      bash "$V" --help >/dev/null 2>&1; expect_exit "--help exits 0" 0 $?
      bash "$V" -h     >/dev/null 2>&1; expect_exit "-h exits 0" 0 $?
      out="$(bash "$V" --help 2>/dev/null)"
      expect_has "--help has Examples" "xamples" "$out"
      expect_has "--help lists --async" "--async" "$out"
      expect_has "--help lists --db"    "--db" "$out"
      expect_has "--help lists --api"   "--api" "$out"
      # --help must short-circuit before any side effect (no file created even though
      # the cwd has no tests/ dir)
      mkdir -p "$SB/help-cwd"
      ( cd "$SB/help-cwd" && bash "$V" --help >/dev/null 2>&1 )
      [[ ! -e "$SB/help-cwd/tests" ]] && ok "--help creates no files" || no "--help created files"
      
      # ── fresh generation ──────────────────────────────────────────────────────────
      echo "-- fresh generation --"
      mkdir -p "$SB/fresh"
      ( cd "$SB/fresh" && bash "$V" </dev/null >"$SB/fresh.out" 2>"$SB/fresh.err" ); expect_exit "fresh generate -> 0" 0 $?
      [[ -f "$SB/fresh/tests/conftest.py" ]] && ok "creates tests/conftest.py" || no "conftest.py not created"
      out="$(cat "$SB/fresh/tests/conftest.py")"
      expect_has "has module docstring"      "Pytest configuration and fixtures" "$out"
      expect_has "imports pytest"            "import pytest" "$out"
      expect_has "registers slow marker"     '"slow: marks tests as slow"' "$out"
      expect_has "has sample_data fixture"   "def sample_data():" "$out"
      expect_has "has temp_file fixture"     "def temp_file(tmp_path):" "$out"
      expect_has "has pytest_addoption"      "def pytest_addoption(parser):" "$out"
      expect_has "summary on stdout"         "Generated tests/conftest.py" "$(cat "$SB/fresh.out")"
      
      # ── fixture-block flags ───────────────────────────────────────────────────────
      echo "-- fixture blocks --"
      mkdir -p "$SB/all"
      ( cd "$SB/all" && bash "$V" --async --db --api </dev/null >/dev/null 2>&1 ); expect_exit "all flags -> 0" 0 $?
      out="$(cat "$SB/all/tests/conftest.py")"
      expect_has "--async adds event_loop"    "def event_loop():" "$out"
      expect_has "--async adds async_client"  "async def async_client():" "$out"
      expect_has "--db adds db_engine"        "def db_engine():" "$out"
      expect_has "--db adds db_session"       "def db_session(db_engine):" "$out"
      expect_has "--api adds client fixture"  "def client(app):" "$out"
      expect_has "--api adds authed client"   "def authenticated_client(client):" "$out"
      
      # base generation must NOT include the optional blocks
      ! grep -q "def event_loop" "$SB/fresh/tests/conftest.py" && ok "base omits async blocks" || no "base included async blocks"
      ! grep -q "def db_engine"  "$SB/fresh/tests/conftest.py" && ok "base omits db blocks"    || no "base included db blocks"
      ! grep -q "def client"     "$SB/fresh/tests/conftest.py" && ok "base omits api blocks"   || no "base included api blocks"
      
      # ── overwrite safety ─────────────────────────────────────────────────────────
      echo "-- overwrite safety --"
      # Decline ("n", the documented default) MUST preserve an existing user conftest.
      mkdir -p "$SB/ow/tests"
      printf '# USER-SENTINEL: do not overwrite me\nimport user_thing\n' >"$SB/ow/tests/conftest.py"
      ( cd "$SB/ow" && printf 'n\n' | bash "$V" >/dev/null 2>&1 ); expect_exit "decline overwrite -> 0" 0 $?
      out="$(cat "$SB/ow/tests/conftest.py")"
      expect_has "user conftest preserved (decline)" "USER-SENTINEL" "$out"
      expect_has "user import preserved (decline)"   "import user_thing" "$out"
      
      # Confirming ("y") DOES replace the file — that is the documented overwrite path.
      mkdir -p "$SB/ow2/tests"
      printf '# USER-SENTINEL-2\n' >"$SB/ow2/tests/conftest.py"
      ( cd "$SB/ow2" && printf 'y\n' | bash "$V" >/dev/null 2>&1 ); expect_exit "confirm overwrite -> 0" 0 $?
      out="$(cat "$SB/ow2/tests/conftest.py")"
      ! grep -q "USER-SENTINEL-2" "$SB/ow2/tests/conftest.py" && ok "confirm 'y' replaces user conftest" || no "confirm 'y' did not replace"
      expect_has "new conftest has pytest import" "import pytest" "$out"
      
      # Non-interactive (closed stdin, no answer) also keeps the existing file — the
      # safe default when there is no TTY to answer the prompt.
      mkdir -p "$SB/ow3/tests"
      printf '# USER-SENTINEL-3\n' >"$SB/ow3/tests/conftest.py"
      ( cd "$SB/ow3" && bash "$V" </dev/null >/dev/null 2>&1 )
      expect_has "closed-stdin keeps user conftest" "USER-SENTINEL-3" "$(cat "$SB/ow3/tests/conftest.py")"
      
      echo ""
      echo "=== $PASS passed, $FAIL failed ==="
      [[ "$FAIL" -eq 0 ]] || exit 1
      exit 0
      
  • SKILL.md 5 KB
    ---
    name: python-pytest-ops
    description: "pytest testing patterns for Python. Triggers on: pytest, fixture, mark, parametrize, mock, conftest, test coverage, unit test, integration test, pytest.raises."
    license: MIT
    compatibility: "pytest 7.0+, Python 3.9+. Some features require pytest-asyncio, pytest-mock, pytest-cov."
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: python-typing-ops, python-async-ops
    ---
    
    # Python pytest Patterns
    
    Modern pytest patterns for effective testing.
    
    ## Basic Test Structure
    
    ```python
    import pytest
    
    def test_basic():
        """Simple assertion test."""
        assert 1 + 1 == 2
    
    def test_with_description():
        """Descriptive name and docstring."""
        result = calculate_total([1, 2, 3])
        assert result == 6, "Sum should equal 6"
    ```
    
    ## Fixtures
    
    ```python
    import pytest
    
    @pytest.fixture
    def sample_user():
        """Create test user."""
        return {"id": 1, "name": "Test User"}
    
    @pytest.fixture
    def db_connection():
        """Fixture with setup and teardown."""
        conn = create_connection()
        yield conn
        conn.close()
    
    def test_user(sample_user):
        """Fixtures injected by name."""
        assert sample_user["name"] == "Test User"
    ```
    
    ### Fixture Scopes
    
    ```python
    @pytest.fixture(scope="function")  # Default - per test
    @pytest.fixture(scope="class")     # Per test class
    @pytest.fixture(scope="module")    # Per test file
    @pytest.fixture(scope="session")   # Entire test run
    ```
    
    ## Parametrize
    
    ```python
    @pytest.mark.parametrize("input,expected", [
        (1, 2),
        (2, 4),
        (3, 6),
    ])
    def test_double(input, expected):
        assert double(input) == expected
    
    # Multiple parameters
    @pytest.mark.parametrize("x", [1, 2])
    @pytest.mark.parametrize("y", [10, 20])
    def test_multiply(x, y):  # 4 test combinations
        assert x * y > 0
    ```
    
    ## Exception Testing
    
    ```python
    def test_raises():
        with pytest.raises(ValueError) as exc_info:
            raise ValueError("Invalid input")
        assert "Invalid" in str(exc_info.value)
    
    def test_raises_match():
        with pytest.raises(ValueError, match=r".*[Ii]nvalid.*"):
            raise ValueError("Invalid input")
    ```
    
    ## Markers
    
    ```python
    @pytest.mark.skip(reason="Not implemented yet")
    def test_future_feature():
        pass
    
    @pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
    def test_unix_feature():
        pass
    
    @pytest.mark.xfail(reason="Known bug")
    def test_buggy():
        assert broken_function() == expected
    
    @pytest.mark.slow
    def test_performance():
        """Custom marker - register in pytest.ini."""
        pass
    ```
    
    ## Mocking
    
    ```python
    from unittest.mock import Mock, patch, MagicMock
    
    def test_with_mock():
        mock_api = Mock()
        mock_api.get.return_value = {"status": "ok"}
        result = mock_api.get("/endpoint")
        assert result["status"] == "ok"
    
    @patch("module.external_api")
    def test_with_patch(mock_api):
        mock_api.return_value = {"data": []}
        result = function_using_api()
        mock_api.assert_called_once()
    ```
    
    ### pytest-mock (Recommended)
    
    ```python
    def test_with_mocker(mocker):
        mock_api = mocker.patch("module.api_call")
        mock_api.return_value = {"success": True}
        result = process_data()
        assert result["success"]
    ```
    
    ## conftest.py
    
    ```python
    # tests/conftest.py - Shared fixtures
    
    import pytest
    
    @pytest.fixture(scope="session")
    def app():
        """Application fixture available to all tests."""
        return create_app(testing=True)
    
    @pytest.fixture
    def client(app):
        """Test client fixture."""
        return app.test_client()
    ```
    
    ## Quick Reference
    
    Run these inside the project env — prefix with `uv run` (e.g. `uv run pytest -v`).
    Bare `pytest` is shown below for brevity.
    
    | Command | Description |
    |---------|-------------|
    | `pytest` | Run all tests |
    | `pytest -v` | Verbose output |
    | `pytest -x` | Stop on first failure |
    | `pytest -k "test_name"` | Run matching tests |
    | `pytest -m slow` | Run marked tests |
    | `pytest --lf` | Rerun last failed |
    | `pytest --cov=src` | Coverage report |
    | `pytest -n auto` | Parallel (pytest-xdist) |
    
    ## Additional Resources
    
    - `./references/fixtures-advanced.md` - Factory fixtures, autouse, conftest patterns
    - `./references/mocking-patterns.md` - Mock, patch, MagicMock, side_effect
    - `./references/async-testing.md` - pytest-asyncio patterns
    - `./references/coverage-strategies.md` - pytest-cov, branch coverage, reports
    - `./references/integration-testing.md` - Database fixtures, API testing, testcontainers
    - `./references/property-testing.md` - Hypothesis framework, strategies, shrinking
    - `./references/test-architecture.md` - Test pyramid, organization, isolation strategies
    
    ## Scripts
    
    - `./scripts/run-tests.sh` - Run tests with recommended options
    - `./scripts/generate-conftest.sh` - Generate conftest.py boilerplate
    
    ## Assets
    
    - `./assets/pytest.ini.template` - Recommended pytest configuration
    - `./assets/conftest.py.template` - Common fixture patterns
    
    ---
    
    ## See Also
    
    **Related Skills:**
    - `python-typing-ops` - Type-safe test code
    - `python-async-ops` - Async test patterns (pytest-asyncio)
    
    **Testing specific frameworks:**
    - `python-fastapi-ops` - TestClient, API testing
    - `python-database-ops` - Database fixtures, transactions
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related