Claude Cursor Skill

python-testing-patterns

Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.

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

Full trust report

Download wshobson-agents-plugins_python-development_skills_python-testing-patterns-554237f.zip · 8 KB
Part of wshobson/agents — 170 skills

Install

skills CLI npx skills add https://github.com/wshobson/agents/tree/main/plugins/python-development/skills/python-testing-patterns
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
Git git clone https://github.com/wshobson/agents.git

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

Skill manifest

Python Testing Patterns

Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.

When to Use This Skill

  • Writing unit tests for Python code
  • Setting up test suites and test infrastructure
  • Implementing test-driven development (TDD)
  • Creating integration tests for APIs and services
  • Mocking external dependencies and services
  • Testing async code and concurrent operations
  • Setting up continuous testing in CI/CD
  • Implementing property-based testing
  • Testing database operations
  • Debugging failing tests

Core Concepts

1. Test Types

  • Unit Tests: Test individual functions/classes in isolation
  • Integration Tests: Test interaction between components
  • Functional Tests: Test complete features end-to-end
  • Performance Tests: Measure speed and resource usage

2. Test Structure (AAA Pattern)

  • Arrange: Set up test data and preconditions
  • Act: Execute the code under test
  • Assert: Verify the results

3. Test Coverage

  • Measure what code is exercised by tests
  • Identify untested code paths
  • Aim for meaningful coverage, not just high percentages

4. Test Isolation

  • Tests should be independent
  • No shared state between tests
  • Each test should clean up after itself

Quick Start

# test_example.py
def add(a, b):
    return a + b

def test_add():
    """Basic test example."""
    result = add(2, 3)
    assert result == 5

def test_add_negative():
    """Test with negative numbers."""
    assert add(-1, 1) == 0

# Run with: pytest test_example.py

Detailed patterns and worked examples

Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.

Testing Best Practices

Test Organization

# tests/
#   __init__.py
#   conftest.py           # Shared fixtures
#   test_unit/            # Unit tests
#     test_models.py
#     test_utils.py
#   test_integration/     # Integration tests
#     test_api.py
#     test_database.py
#   test_e2e/            # End-to-end tests
#     test_workflows.py

Test Naming Convention

A common pattern: test_<unit>_<scenario>_<expected_outcome>. Adapt to your team's preferences.

# Pattern: test_<unit>_<scenario>_<expected>
def test_create_user_with_valid_data_returns_user():
    ...

def test_create_user_with_duplicate_email_raises_conflict():
    ...

def test_get_user_with_unknown_id_returns_none():
    ...

# Good test names - clear and descriptive
def test_user_creation_with_valid_data():
    """Clear name describes what is being tested."""
    pass

def test_login_fails_with_invalid_password():
    """Name describes expected behavior."""
    pass

def test_api_returns_404_for_missing_resource():
    """Specific about inputs and expected outcomes."""
    pass

# Bad test names - avoid these
def test_1():  # Not descriptive
    pass

def test_user():  # Too vague
    pass

def test_function():  # Doesn't explain what's tested
    pass

Testing Retry Behavior

Verify that retry logic works correctly using mock side effects.

from unittest.mock import Mock

def test_retries_on_transient_error():
    """Test that service retries on transient failures."""
    client = Mock()
    # Fail twice, then succeed
    client.request.side_effect = [
        ConnectionError("Failed"),
        ConnectionError("Failed"),
        {"status": "ok"},
    ]

    service = ServiceWithRetry(client, max_retries=3)
    result = service.fetch()

    assert result == {"status": "ok"}
    assert client.request.call_count == 3

def test_gives_up_after_max_retries():
    """Test that service stops retrying after max attempts."""
    client = Mock()
    client.request.side_effect = ConnectionError("Failed")

    service = ServiceWithRetry(client, max_retries=3)

    with pytest.raises(ConnectionError):
        service.fetch()

    assert client.request.call_count == 3

def test_does_not_retry_on_permanent_error():
    """Test that permanent errors are not retried."""
    client = Mock()
    client.request.side_effect = ValueError("Invalid input")

    service = ServiceWithRetry(client, max_retries=3)

    with pytest.raises(ValueError):
        service.fetch()

    # Only called once - no retry for ValueError
    assert client.request.call_count == 1

Mocking Time with Freezegun

Use freezegun to control time in tests for predictable time-dependent behavior.

from freezegun import freeze_time
from datetime import datetime, timedelta

@freeze_time("2026-01-15 10:00:00")
def test_token_expiry():
    """Test token expires at correct time."""
    token = create_token(expires_in_seconds=3600)
    assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)

@freeze_time("2026-01-15 10:00:00")
def test_is_expired_returns_false_before_expiry():
    """Test token is not expired when within validity period."""
    token = create_token(expires_in_seconds=3600)
    assert not token.is_expired()

@freeze_time("2026-01-15 12:00:00")
def test_is_expired_returns_true_after_expiry():
    """Test token is expired after validity period."""
    token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
    assert token.is_expired()

def test_with_time_travel():
    """Test behavior across time using freeze_time context."""
    with freeze_time("2026-01-01") as frozen_time:
        item = create_item()
        assert item.created_at == datetime(2026, 1, 1)

        # Move forward in time
        frozen_time.move_to("2026-01-15")
        assert item.age_days == 14

Test Markers

# test_markers.py
import pytest

@pytest.mark.slow
def test_slow_operation():
    """Mark slow tests."""
    import time
    time.sleep(2)


@pytest.mark.integration
def test_database_integration():
    """Mark integration tests."""
    pass


@pytest.mark.skip(reason="Feature not implemented yet")
def test_future_feature():
    """Skip tests temporarily."""
    pass


@pytest.mark.skipif(os.name == "nt", reason="Unix only test")
def test_unix_specific():
    """Conditional skip."""
    pass


@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
    """Mark expected failures."""
    assert False


# Run with:
# pytest -m slow          # Run only slow tests
# pytest -m "not slow"    # Skip slow tests
# pytest -m integration   # Run integration tests

Coverage Reporting

# Install coverage
pip install pytest-cov

# Run tests with coverage
pytest --cov=myapp tests/

# Generate HTML report
pytest --cov=myapp --cov-report=html tests/

# Fail if coverage below threshold
pytest --cov=myapp --cov-fail-under=80 tests/

# Show missing lines
pytest --cov=myapp --cov-report=term-missing tests/

For advanced patterns (async testing, monkeypatching, property-based testing, database testing, CI/CD integration, and configuration), see references/advanced-patterns.md

Files (agents)
  • references
    • advanced-patterns.md 9.2 KB
      # Python Testing Patterns — Advanced Reference
      
      Advanced testing patterns including async code, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration.
      
      ## Pattern 6: Testing Async Code
      
      ```python
      # test_async.py
      import pytest
      import asyncio
      
      async def fetch_data(url: str) -> dict:
          """Fetch data asynchronously."""
          await asyncio.sleep(0.1)
          return {"url": url, "data": "result"}
      
      
      @pytest.mark.asyncio
      async def test_fetch_data():
          """Test async function."""
          result = await fetch_data("https://api.example.com")
          assert result["url"] == "https://api.example.com"
          assert "data" in result
      
      
      @pytest.mark.asyncio
      async def test_concurrent_fetches():
          """Test concurrent async operations."""
          urls = ["url1", "url2", "url3"]
          tasks = [fetch_data(url) for url in urls]
          results = await asyncio.gather(*tasks)
      
          assert len(results) == 3
          assert all("data" in r for r in results)
      
      
      @pytest.fixture
      async def async_client():
          """Async fixture."""
          client = {"connected": True}
          yield client
          client["connected"] = False
      
      
      @pytest.mark.asyncio
      async def test_with_async_fixture(async_client):
          """Test using async fixture."""
          assert async_client["connected"] is True
      ```
      
      ## Pattern 7: Monkeypatch for Testing
      
      ```python
      # test_environment.py
      import os
      import pytest
      
      def get_database_url() -> str:
          """Get database URL from environment."""
          return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
      
      
      def test_database_url_default():
          """Test default database URL."""
          # Will use actual environment variable if set
          url = get_database_url()
          assert url
      
      
      def test_database_url_custom(monkeypatch):
          """Test custom database URL with monkeypatch."""
          monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
          assert get_database_url() == "postgresql://localhost/test"
      
      
      def test_database_url_not_set(monkeypatch):
          """Test when env var is not set."""
          monkeypatch.delenv("DATABASE_URL", raising=False)
          assert get_database_url() == "sqlite:///:memory:"
      
      
      class Config:
          """Configuration class."""
      
          def __init__(self):
              self.api_key = "production-key"
      
          def get_api_key(self):
              return self.api_key
      
      
      def test_monkeypatch_attribute(monkeypatch):
          """Test monkeypatching object attributes."""
          config = Config()
          monkeypatch.setattr(config, "api_key", "test-key")
          assert config.get_api_key() == "test-key"
      ```
      
      ## Pattern 8: Temporary Files and Directories
      
      ```python
      # test_file_operations.py
      import pytest
      from pathlib import Path
      
      def save_data(filepath: Path, data: str):
          """Save data to file."""
          filepath.write_text(data)
      
      
      def load_data(filepath: Path) -> str:
          """Load data from file."""
          return filepath.read_text()
      
      
      def test_file_operations(tmp_path):
          """Test file operations with temporary directory."""
          # tmp_path is a pathlib.Path object
          test_file = tmp_path / "test_data.txt"
      
          # Save data
          save_data(test_file, "Hello, World!")
      
          # Verify file exists
          assert test_file.exists()
      
          # Load and verify data
          data = load_data(test_file)
          assert data == "Hello, World!"
      
      
      def test_multiple_files(tmp_path):
          """Test with multiple temporary files."""
          files = {
              "file1.txt": "Content 1",
              "file2.txt": "Content 2",
              "file3.txt": "Content 3"
          }
      
          for filename, content in files.items():
              filepath = tmp_path / filename
              save_data(filepath, content)
      
          # Verify all files created
          assert len(list(tmp_path.iterdir())) == 3
      
          # Verify contents
          for filename, expected_content in files.items():
              filepath = tmp_path / filename
              assert load_data(filepath) == expected_content
      ```
      
      ## Pattern 9: Custom Fixtures and Conftest
      
      ```python
      # conftest.py
      """Shared fixtures for all tests."""
      import pytest
      
      @pytest.fixture(scope="session")
      def database_url():
          """Provide database URL for all tests."""
          return "postgresql://localhost/test_db"
      
      
      @pytest.fixture(autouse=True)
      def reset_database(database_url):
          """Auto-use fixture that runs before each test."""
          # Setup: Clear database
          print(f"Clearing database: {database_url}")
          yield
          # Teardown: Clean up
          print("Test completed")
      
      
      @pytest.fixture
      def sample_user():
          """Provide sample user data."""
          return {
              "id": 1,
              "name": "Test User",
              "email": "test@example.com"
          }
      
      
      @pytest.fixture
      def sample_users():
          """Provide list of sample users."""
          return [
              {"id": 1, "name": "User 1"},
              {"id": 2, "name": "User 2"},
              {"id": 3, "name": "User 3"},
          ]
      
      
      # Parametrized fixture
      @pytest.fixture(params=["sqlite", "postgresql", "mysql"])
      def db_backend(request):
          """Fixture that runs tests with different database backends."""
          return request.param
      
      
      def test_with_db_backend(db_backend):
          """This test will run 3 times with different backends."""
          print(f"Testing with {db_backend}")
          assert db_backend in ["sqlite", "postgresql", "mysql"]
      ```
      
      ## Pattern 10: Property-Based Testing
      
      ```python
      # test_properties.py
      from hypothesis import given, strategies as st
      import pytest
      
      def reverse_string(s: str) -> str:
          """Reverse a string."""
          return s[::-1]
      
      
      @given(st.text())
      def test_reverse_twice_is_original(s):
          """Property: reversing twice returns original."""
          assert reverse_string(reverse_string(s)) == s
      
      
      @given(st.text())
      def test_reverse_length(s):
          """Property: reversed string has same length."""
          assert len(reverse_string(s)) == len(s)
      
      
      @given(st.integers(), st.integers())
      def test_addition_commutative(a, b):
          """Property: addition is commutative."""
          assert a + b == b + a
      
      
      @given(st.lists(st.integers()))
      def test_sorted_list_properties(lst):
          """Property: sorted list is ordered."""
          sorted_lst = sorted(lst)
      
          # Same length
          assert len(sorted_lst) == len(lst)
      
          # All elements present
          assert set(sorted_lst) == set(lst)
      
          # Is ordered
          for i in range(len(sorted_lst) - 1):
              assert sorted_lst[i] <= sorted_lst[i + 1]
      ```
      
      ## Testing Database Code
      
      ```python
      # test_database_models.py
      import pytest
      from sqlalchemy import create_engine, Column, Integer, String
      from sqlalchemy.ext.declarative import declarative_base
      from sqlalchemy.orm import sessionmaker, Session
      
      Base = declarative_base()
      
      
      class User(Base):
          """User model."""
          __tablename__ = "users"
      
          id = Column(Integer, primary_key=True)
          name = Column(String(50))
          email = Column(String(100), unique=True)
      
      
      @pytest.fixture(scope="function")
      def db_session() -> Session:
          """Create in-memory database for testing."""
          engine = create_engine("sqlite:///:memory:")
          Base.metadata.create_all(engine)
      
          SessionLocal = sessionmaker(bind=engine)
          session = SessionLocal()
      
          yield session
      
          session.close()
      
      
      def test_create_user(db_session):
          """Test creating a user."""
          user = User(name="Test User", email="test@example.com")
          db_session.add(user)
          db_session.commit()
      
          assert user.id is not None
          assert user.name == "Test User"
      
      
      def test_query_user(db_session):
          """Test querying users."""
          user1 = User(name="User 1", email="user1@example.com")
          user2 = User(name="User 2", email="user2@example.com")
      
          db_session.add_all([user1, user2])
          db_session.commit()
      
          users = db_session.query(User).all()
          assert len(users) == 2
      
      
      def test_unique_email_constraint(db_session):
          """Test unique email constraint."""
          from sqlalchemy.exc import IntegrityError
      
          user1 = User(name="User 1", email="same@example.com")
          user2 = User(name="User 2", email="same@example.com")
      
          db_session.add(user1)
          db_session.commit()
      
          db_session.add(user2)
      
          with pytest.raises(IntegrityError):
              db_session.commit()
      ```
      
      ## CI/CD Integration
      
      ```yaml
      # .github/workflows/test.yml
      name: Tests
      
      on: [push, pull_request]
      
      jobs:
        test:
          runs-on: ubuntu-latest
      
          strategy:
            matrix:
              python-version: ["3.9", "3.10", "3.11", "3.12"]
      
          steps:
            - uses: actions/checkout@v3
      
            - name: Set up Python
              uses: actions/setup-python@v4
              with:
                python-version: ${{ matrix.python-version }}
      
            - name: Install dependencies
              run: |
                pip install -e ".[dev]"
                pip install pytest pytest-cov
      
            - name: Run tests
              run: |
                pytest --cov=myapp --cov-report=xml
      
            - name: Upload coverage
              uses: codecov/codecov-action@v3
              with:
                file: ./coverage.xml
      ```
      
      ## Configuration Files
      
      ```ini
      # pytest.ini
      [pytest]
      testpaths = tests
      python_files = test_*.py
      python_classes = Test*
      python_functions = test_*
      addopts =
          -v
          --strict-markers
          --tb=short
          --cov=myapp
          --cov-report=term-missing
      markers =
          slow: marks tests as slow
          integration: marks integration tests
          unit: marks unit tests
          e2e: marks end-to-end tests
      ```
      
      ```toml
      # pyproject.toml
      [tool.pytest.ini_options]
      testpaths = ["tests"]
      python_files = ["test_*.py"]
      addopts = [
          "-v",
          "--cov=myapp",
          "--cov-report=term-missing",
      ]
      
      [tool.coverage.run]
      source = ["myapp"]
      omit = ["*/tests/*", "*/migrations/*"]
      
      [tool.coverage.report]
      exclude_lines = [
          "pragma: no cover",
          "def __repr__",
          "raise AssertionError",
          "raise NotImplementedError",
      ]
      ```
      
    • details.md 9.1 KB
      # python-testing-patterns — detailed patterns and worked examples
      
      ## Fundamental Patterns
      
      ### Pattern 1: Basic pytest Tests
      
      ```python
      # test_calculator.py
      import pytest
      
      class Calculator:
          """Simple calculator for testing."""
      
          def add(self, a: float, b: float) -> float:
              return a + b
      
          def subtract(self, a: float, b: float) -> float:
              return a - b
      
          def multiply(self, a: float, b: float) -> float:
              return a * b
      
          def divide(self, a: float, b: float) -> float:
              if b == 0:
                  raise ValueError("Cannot divide by zero")
              return a / b
      
      
      def test_addition():
          """Test addition."""
          calc = Calculator()
          assert calc.add(2, 3) == 5
          assert calc.add(-1, 1) == 0
          assert calc.add(0, 0) == 0
      
      
      def test_subtraction():
          """Test subtraction."""
          calc = Calculator()
          assert calc.subtract(5, 3) == 2
          assert calc.subtract(0, 5) == -5
      
      
      def test_multiplication():
          """Test multiplication."""
          calc = Calculator()
          assert calc.multiply(3, 4) == 12
          assert calc.multiply(0, 5) == 0
      
      
      def test_division():
          """Test division."""
          calc = Calculator()
          assert calc.divide(6, 3) == 2
          assert calc.divide(5, 2) == 2.5
      
      
      def test_division_by_zero():
          """Test division by zero raises error."""
          calc = Calculator()
          with pytest.raises(ValueError, match="Cannot divide by zero"):
              calc.divide(5, 0)
      ```
      
      ### Pattern 2: Fixtures for Setup and Teardown
      
      ```python
      # test_database.py
      import pytest
      from typing import Generator
      
      class Database:
          """Simple database class."""
      
          def __init__(self, connection_string: str):
              self.connection_string = connection_string
              self.connected = False
      
          def connect(self):
              """Connect to database."""
              self.connected = True
      
          def disconnect(self):
              """Disconnect from database."""
              self.connected = False
      
          def query(self, sql: str) -> list:
              """Execute query."""
              if not self.connected:
                  raise RuntimeError("Not connected")
              return [{"id": 1, "name": "Test"}]
      
      
      @pytest.fixture
      def db() -> Generator[Database, None, None]:
          """Fixture that provides connected database."""
          # Setup
          database = Database("sqlite:///:memory:")
          database.connect()
      
          # Provide to test
          yield database
      
          # Teardown
          database.disconnect()
      
      
      def test_database_query(db):
          """Test database query with fixture."""
          results = db.query("SELECT * FROM users")
          assert len(results) == 1
          assert results[0]["name"] == "Test"
      
      
      @pytest.fixture(scope="session")
      def app_config():
          """Session-scoped fixture - created once per test session."""
          return {
              "database_url": "postgresql://localhost/test",
              "api_key": "test-key",
              "debug": True
          }
      
      
      @pytest.fixture(scope="module")
      def api_client(app_config):
          """Module-scoped fixture - created once per test module."""
          # Setup expensive resource
          client = {"config": app_config, "session": "active"}
          yield client
          # Cleanup
          client["session"] = "closed"
      
      
      def test_api_client(api_client):
          """Test using api client fixture."""
          assert api_client["session"] == "active"
          assert api_client["config"]["debug"] is True
      ```
      
      ### Pattern 3: Parameterized Tests
      
      ```python
      # test_validation.py
      import pytest
      
      def is_valid_email(email: str) -> bool:
          """Check if email is valid."""
          return "@" in email and "." in email.split("@")[1]
      
      
      @pytest.mark.parametrize("email,expected", [
          ("user@example.com", True),
          ("test.user@domain.co.uk", True),
          ("invalid.email", False),
          ("@example.com", False),
          ("user@domain", False),
          ("", False),
      ])
      def test_email_validation(email, expected):
          """Test email validation with various inputs."""
          assert is_valid_email(email) == expected
      
      
      @pytest.mark.parametrize("a,b,expected", [
          (2, 3, 5),
          (0, 0, 0),
          (-1, 1, 0),
          (100, 200, 300),
          (-5, -5, -10),
      ])
      def test_addition_parameterized(a, b, expected):
          """Test addition with multiple parameter sets."""
          from test_calculator import Calculator
          calc = Calculator()
          assert calc.add(a, b) == expected
      
      
      # Using pytest.param for special cases
      @pytest.mark.parametrize("value,expected", [
          pytest.param(1, True, id="positive"),
          pytest.param(0, False, id="zero"),
          pytest.param(-1, False, id="negative"),
      ])
      def test_is_positive(value, expected):
          """Test with custom test IDs."""
          assert (value > 0) == expected
      ```
      
      ### Pattern 4: Mocking with unittest.mock
      
      ```python
      # test_api_client.py
      import pytest
      from unittest.mock import Mock, patch, MagicMock
      import requests
      
      class APIClient:
          """Simple API client."""
      
          def __init__(self, base_url: str):
              self.base_url = base_url
      
          def get_user(self, user_id: int) -> dict:
              """Fetch user from API."""
              response = requests.get(f"{self.base_url}/users/{user_id}")
              response.raise_for_status()
              return response.json()
      
          def create_user(self, data: dict) -> dict:
              """Create new user."""
              response = requests.post(f"{self.base_url}/users", json=data)
              response.raise_for_status()
              return response.json()
      
      
      def test_get_user_success():
          """Test successful API call with mock."""
          client = APIClient("https://api.example.com")
      
          mock_response = Mock()
          mock_response.json.return_value = {"id": 1, "name": "John Doe"}
          mock_response.raise_for_status.return_value = None
      
          with patch("requests.get", return_value=mock_response) as mock_get:
              user = client.get_user(1)
      
              assert user["id"] == 1
              assert user["name"] == "John Doe"
              mock_get.assert_called_once_with("https://api.example.com/users/1")
      
      
      def test_get_user_not_found():
          """Test API call with 404 error."""
          client = APIClient("https://api.example.com")
      
          mock_response = Mock()
          mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
      
          with patch("requests.get", return_value=mock_response):
              with pytest.raises(requests.HTTPError):
                  client.get_user(999)
      
      
      @patch("requests.post")
      def test_create_user(mock_post):
          """Test user creation with decorator syntax."""
          client = APIClient("https://api.example.com")
      
          mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}
          mock_post.return_value.raise_for_status.return_value = None
      
          user_data = {"name": "Jane Doe", "email": "jane@example.com"}
          result = client.create_user(user_data)
      
          assert result["id"] == 2
          mock_post.assert_called_once()
          call_args = mock_post.call_args
          assert call_args.kwargs["json"] == user_data
      ```
      
      ### Pattern 5: Testing Exceptions
      
      ```python
      # test_exceptions.py
      import pytest
      
      def divide(a: float, b: float) -> float:
          """Divide a by b."""
          if b == 0:
              raise ZeroDivisionError("Division by zero")
          if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
              raise TypeError("Arguments must be numbers")
          return a / b
      
      
      def test_zero_division():
          """Test exception is raised for division by zero."""
          with pytest.raises(ZeroDivisionError):
              divide(10, 0)
      
      
      def test_zero_division_with_message():
          """Test exception message."""
          with pytest.raises(ZeroDivisionError, match="Division by zero"):
              divide(5, 0)
      
      
      def test_type_error():
          """Test type error exception."""
          with pytest.raises(TypeError, match="must be numbers"):
              divide("10", 5)
      
      
      def test_exception_info():
          """Test accessing exception info."""
          with pytest.raises(ValueError) as exc_info:
              int("not a number")
      
          assert "invalid literal" in str(exc_info.value)
      ```
      
      For advanced patterns including async testing, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration files, see [references/advanced-patterns.md](references/advanced-patterns.md)
      
      ## Test Design Principles
      
      ### One Behavior Per Test
      
      Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.
      
      ```python
      # BAD - testing multiple behaviors
      def test_user_service():
          user = service.create_user(data)
          assert user.id is not None
          assert user.email == data["email"]
          updated = service.update_user(user.id, {"name": "New"})
          assert updated.name == "New"
      
      # GOOD - focused tests
      def test_create_user_assigns_id():
          user = service.create_user(data)
          assert user.id is not None
      
      def test_create_user_stores_email():
          user = service.create_user(data)
          assert user.email == data["email"]
      
      def test_update_user_changes_name():
          user = service.create_user(data)
          updated = service.update_user(user.id, {"name": "New"})
          assert updated.name == "New"
      ```
      
      ### Test Error Paths
      
      Always test failure cases, not just happy paths.
      
      ```python
      def test_get_user_raises_not_found():
          with pytest.raises(UserNotFoundError) as exc_info:
              service.get_user("nonexistent-id")
      
          assert "nonexistent-id" in str(exc_info.value)
      
      def test_create_user_rejects_invalid_email():
          with pytest.raises(ValueError, match="Invalid email format"):
              service.create_user({"email": "not-an-email"})
      ```
      
  • SKILL.md 7.1 KB
    ---
    name: python-testing-patterns
    description: Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
    ---
    
    # Python Testing Patterns
    
    Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.
    
    ## When to Use This Skill
    
    - Writing unit tests for Python code
    - Setting up test suites and test infrastructure
    - Implementing test-driven development (TDD)
    - Creating integration tests for APIs and services
    - Mocking external dependencies and services
    - Testing async code and concurrent operations
    - Setting up continuous testing in CI/CD
    - Implementing property-based testing
    - Testing database operations
    - Debugging failing tests
    
    ## Core Concepts
    
    ### 1. Test Types
    
    - **Unit Tests**: Test individual functions/classes in isolation
    - **Integration Tests**: Test interaction between components
    - **Functional Tests**: Test complete features end-to-end
    - **Performance Tests**: Measure speed and resource usage
    
    ### 2. Test Structure (AAA Pattern)
    
    - **Arrange**: Set up test data and preconditions
    - **Act**: Execute the code under test
    - **Assert**: Verify the results
    
    ### 3. Test Coverage
    
    - Measure what code is exercised by tests
    - Identify untested code paths
    - Aim for meaningful coverage, not just high percentages
    
    ### 4. Test Isolation
    
    - Tests should be independent
    - No shared state between tests
    - Each test should clean up after itself
    
    ## Quick Start
    
    ```python
    # test_example.py
    def add(a, b):
        return a + b
    
    def test_add():
        """Basic test example."""
        result = add(2, 3)
        assert result == 5
    
    def test_add_negative():
        """Test with negative numbers."""
        assert add(-1, 1) == 0
    
    # Run with: pytest test_example.py
    ```
    
    ## Detailed patterns and worked examples
    
    Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
    
    ## Testing Best Practices
    
    ### Test Organization
    
    ```python
    # tests/
    #   __init__.py
    #   conftest.py           # Shared fixtures
    #   test_unit/            # Unit tests
    #     test_models.py
    #     test_utils.py
    #   test_integration/     # Integration tests
    #     test_api.py
    #     test_database.py
    #   test_e2e/            # End-to-end tests
    #     test_workflows.py
    ```
    
    ### Test Naming Convention
    
    A common pattern: `test_<unit>_<scenario>_<expected_outcome>`. Adapt to your team's preferences.
    
    ```python
    # Pattern: test_<unit>_<scenario>_<expected>
    def test_create_user_with_valid_data_returns_user():
        ...
    
    def test_create_user_with_duplicate_email_raises_conflict():
        ...
    
    def test_get_user_with_unknown_id_returns_none():
        ...
    
    # Good test names - clear and descriptive
    def test_user_creation_with_valid_data():
        """Clear name describes what is being tested."""
        pass
    
    def test_login_fails_with_invalid_password():
        """Name describes expected behavior."""
        pass
    
    def test_api_returns_404_for_missing_resource():
        """Specific about inputs and expected outcomes."""
        pass
    
    # Bad test names - avoid these
    def test_1():  # Not descriptive
        pass
    
    def test_user():  # Too vague
        pass
    
    def test_function():  # Doesn't explain what's tested
        pass
    ```
    
    ### Testing Retry Behavior
    
    Verify that retry logic works correctly using mock side effects.
    
    ```python
    from unittest.mock import Mock
    
    def test_retries_on_transient_error():
        """Test that service retries on transient failures."""
        client = Mock()
        # Fail twice, then succeed
        client.request.side_effect = [
            ConnectionError("Failed"),
            ConnectionError("Failed"),
            {"status": "ok"},
        ]
    
        service = ServiceWithRetry(client, max_retries=3)
        result = service.fetch()
    
        assert result == {"status": "ok"}
        assert client.request.call_count == 3
    
    def test_gives_up_after_max_retries():
        """Test that service stops retrying after max attempts."""
        client = Mock()
        client.request.side_effect = ConnectionError("Failed")
    
        service = ServiceWithRetry(client, max_retries=3)
    
        with pytest.raises(ConnectionError):
            service.fetch()
    
        assert client.request.call_count == 3
    
    def test_does_not_retry_on_permanent_error():
        """Test that permanent errors are not retried."""
        client = Mock()
        client.request.side_effect = ValueError("Invalid input")
    
        service = ServiceWithRetry(client, max_retries=3)
    
        with pytest.raises(ValueError):
            service.fetch()
    
        # Only called once - no retry for ValueError
        assert client.request.call_count == 1
    ```
    
    ### Mocking Time with Freezegun
    
    Use freezegun to control time in tests for predictable time-dependent behavior.
    
    ```python
    from freezegun import freeze_time
    from datetime import datetime, timedelta
    
    @freeze_time("2026-01-15 10:00:00")
    def test_token_expiry():
        """Test token expires at correct time."""
        token = create_token(expires_in_seconds=3600)
        assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)
    
    @freeze_time("2026-01-15 10:00:00")
    def test_is_expired_returns_false_before_expiry():
        """Test token is not expired when within validity period."""
        token = create_token(expires_in_seconds=3600)
        assert not token.is_expired()
    
    @freeze_time("2026-01-15 12:00:00")
    def test_is_expired_returns_true_after_expiry():
        """Test token is expired after validity period."""
        token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
        assert token.is_expired()
    
    def test_with_time_travel():
        """Test behavior across time using freeze_time context."""
        with freeze_time("2026-01-01") as frozen_time:
            item = create_item()
            assert item.created_at == datetime(2026, 1, 1)
    
            # Move forward in time
            frozen_time.move_to("2026-01-15")
            assert item.age_days == 14
    ```
    
    ### Test Markers
    
    ```python
    # test_markers.py
    import pytest
    
    @pytest.mark.slow
    def test_slow_operation():
        """Mark slow tests."""
        import time
        time.sleep(2)
    
    
    @pytest.mark.integration
    def test_database_integration():
        """Mark integration tests."""
        pass
    
    
    @pytest.mark.skip(reason="Feature not implemented yet")
    def test_future_feature():
        """Skip tests temporarily."""
        pass
    
    
    @pytest.mark.skipif(os.name == "nt", reason="Unix only test")
    def test_unix_specific():
        """Conditional skip."""
        pass
    
    
    @pytest.mark.xfail(reason="Known bug #123")
    def test_known_bug():
        """Mark expected failures."""
        assert False
    
    
    # Run with:
    # pytest -m slow          # Run only slow tests
    # pytest -m "not slow"    # Skip slow tests
    # pytest -m integration   # Run integration tests
    ```
    
    ### Coverage Reporting
    
    ```bash
    # Install coverage
    pip install pytest-cov
    
    # Run tests with coverage
    pytest --cov=myapp tests/
    
    # Generate HTML report
    pytest --cov=myapp --cov-report=html tests/
    
    # Fail if coverage below threshold
    pytest --cov=myapp --cov-fail-under=80 tests/
    
    # Show missing lines
    pytest --cov=myapp --cov-report=term-missing tests/
    ```
    
    For advanced patterns (async testing, monkeypatching, property-based testing, database testing, CI/CD integration, and configuration), see [references/advanced-patterns.md](references/advanced-patterns.md)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related