Claude Code Plugins

Community-maintained marketplace

Feedback

Python-specific testing practices with pytest, fixtures, mocking, async testing, coverage configuration, and uv execution rules. Activate when working with pytest files, conftest.py, test directories, pyproject.toml testing configuration, or Python test-related tasks.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name python-testing
description Python-specific testing practices with pytest, fixtures, mocking, async testing, coverage configuration, and uv execution rules. Activate when working with pytest files, conftest.py, test directories, pyproject.toml testing configuration, or Python test-related tasks.

Python Testing Practices

Python-specific testing patterns and best practices using pytest, complementing general testing-workflow skill.

CRITICAL: UV Execution Rules

NEVER use -m flag with uv run:

# ✅ CORRECT - UV pytest execution
uv run pytest
uv run pytest -v
uv run pytest tests/unit/ -v
uv run pytest --cov=app --cov-report=html

# ❌ WRONG - Never add -m flag
# ❌ uv run -m pytest
# ❌ uv run -m pytest -v

Always use uv run pytest directly (never call pytest directly in uv projects).


Pytest Fixtures

Fixture Scopes

Pytest fixtures support different scopes for setup/teardown control:

@pytest.fixture(scope="session")
def database_connection():
    """Created once per test session - expensive setup."""
    connection = setup_expensive_database()
    yield connection
    connection.cleanup()


@pytest.fixture(scope="module")
def api_client():
    """Created once per test module."""
    return APIClient(config="test")


@pytest.fixture(scope="class")
def service_instance():
    """Created once per test class."""
    return Service()


@pytest.fixture(scope="function")  # Default
def user():
    """Created for each test function - isolated."""
    return User(email="test@example.com")

Fixture Setup and Teardown

Use yield pattern for setup/teardown with proper cleanup:

@pytest.fixture
def database():
    """Fixture with setup and teardown."""
    # Setup
    db = Database(":memory:")
    db.initialize()
    db.create_schema()

    # Provide to test
    yield db

    # Teardown
    db.close()


@pytest.fixture
def mock_file(tmp_path):
    """Create temporary file that auto-cleans up."""
    temp_file = tmp_path / "test.txt"
    temp_file.write_text("test data")
    yield temp_file
    # Cleanup automatic with tmp_path

Fixture Dependency Injection

Fixtures can depend on other fixtures:

@pytest.fixture
def database():
    """Base database fixture."""
    db = Database(":memory:")
    db.initialize()
    yield db
    db.close()


@pytest.fixture
def user_service(database):
    """Service depending on database fixture."""
    return UserService(database)


@pytest.fixture
def authenticated_user(user_service):
    """User depending on service fixture."""
    user = user_service.create_user(
        username="testuser",
        email="test@example.com",
        password="secure"
    )
    yield user
    user_service.delete_user(user.id)

conftest.py Patterns

Central Fixture Location

Place shared fixtures in conftest.py at appropriate levels:

tests/
├── conftest.py              # Session/module level fixtures
├── unit/
│   ├── conftest.py          # Unit-specific fixtures
│   └── test_*.py
└── integration/
    ├── conftest.py          # Integration-specific fixtures
    └── test_*.py

Example conftest.py

# tests/conftest.py
import pytest
from app.services import UserService
from app.database import Database


@pytest.fixture(scope="session")
def database_connection():
    """Database connection for entire session."""
    db = Database(":memory:")
    db.initialize()
    db.run_migrations()
    yield db
    db.close()


@pytest.fixture
def user_service(database_connection):
    """UserService with test database."""
    return UserService(database_connection)


@pytest.fixture
def sample_user_data():
    """Standard user data for tests."""
    return {
        "username": "testuser",
        "email": "test@example.com",
        "password": "secure123",
        "age": 25
    }


@pytest.fixture
def temp_config_file(tmp_path):
    """Create temporary config file."""
    config_file = tmp_path / "config.json"
    config_file.write_text('{"setting": "value"}')
    return config_file

Parametrized Testing

Basic Parametrization

Use @pytest.mark.parametrize for multiple test inputs:

import pytest


@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("", ""),
    ("MiXeD", "MIXED"),
])
def test_uppercase(input, expected):
    """Test uppercase conversion with multiple inputs."""
    assert input.upper() == expected

Multiple Parameters

@pytest.mark.parametrize("method,expected_status", [
    ("GET", 200),
    ("POST", 201),
    ("PUT", 200),
    ("DELETE", 204),
])
def test_http_methods(client, method, expected_status):
    """Test different HTTP methods."""
    response = client.request(method, "/api/resource")
    assert response.status_code == expected_status

Parametrized Fixtures

@pytest.mark.parametrize("user_role", ["admin", "user", "guest"])
def test_permission_levels(user_role):
    """Test different user permission levels."""
    user = User(role=user_role)
    assert user.can_access_dashboard() == (user_role in ["admin", "user"])

Async Testing

Marking Async Tests

Use @pytest.mark.asyncio decorator:

import pytest


@pytest.mark.asyncio
async def test_async_fetch_data():
    """Test async data fetching."""
    result = await fetch_data_async("user123")
    assert result is not None
    assert result["id"] == "user123"


@pytest.mark.asyncio
async def test_concurrent_requests():
    """Test multiple concurrent async operations."""
    results = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )
    assert len(results) == 3

Async Fixtures

@pytest.fixture
async def async_client():
    """Async HTTP client fixture."""
    client = AsyncHTTPClient()
    await client.connect()
    yield client
    await client.disconnect()


@pytest.mark.asyncio
async def test_api_call(async_client):
    """Test with async fixture."""
    response = await async_client.get("/api/users")
    assert response.status_code == 200

Mocking Patterns

Mock Objects and Patching

from unittest.mock import Mock, patch, MagicMock
import pytest


def test_send_email_success():
    """Test email sending with mocked SMTP."""
    # Create mock object
    mock_smtp = Mock()

    with patch("smtplib.SMTP", return_value=mock_smtp):
        result = send_email("test@example.com", "Subject", "Body")

    # Verify mock was called correctly
    mock_smtp.send_message.assert_called_once()
    assert result is True


def test_api_call_with_mock():
    """Test API call with mocked response."""
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"status": "ok"}

        result = fetch_api_data()

        mock_get.assert_called_once_with("https://api.example.com/data")
        assert result["status"] == "ok"

Fixture-Based Mocking

@pytest.fixture
def mock_database():
    """Provide mocked database."""
    with patch("app.database.Database") as mock_db:
        mock_db.return_value.query.return_value = {"id": 1, "name": "Test"}
        yield mock_db


def test_user_service_with_mock(mock_database):
    """Test service with mocked database dependency."""
    service = UserService(mock_database.return_value)
    user = service.get_user(1)
    assert user["name"] == "Test"


@pytest.fixture
def mock_api_client():
    """Provide mocked API client."""
    with patch("app.client.APIClient") as mock:
        mock.return_value.get.return_value = {"status": "ok"}
        mock.return_value.post.return_value = {"id": 123}
        yield mock

Spy on Real Objects

from unittest.mock import MagicMock


def test_spy_on_method():
    """Spy on actual method calls."""
    obj = RealObject()
    obj.method = MagicMock(side_effect=obj.method)

    result = obj.method("arg1")

    obj.method.assert_called_once_with("arg1")
    assert result == expected_value

Exception Testing

Testing with Context Managers

import pytest
from app.exceptions import UserNotFoundError, ValidationError


def test_exception_raised():
    """Test that correct exception is raised."""
    with pytest.raises(UserNotFoundError) as exc_info:
        user_service.get_user("nonexistent_id")

    assert "nonexistent_id" in str(exc_info.value)


def test_validation_error():
    """Test validation error with message check."""
    user_data = {
        "username": "testuser",
        "email": "invalid-email",  # Invalid format
        "age": 25
    }

    with pytest.raises(ValidationError) as exc_info:
        user_service.create_user(user_data)

    assert "email" in str(exc_info.value)


def test_exception_type_matching():
    """Test matching specific exception type."""
    with pytest.raises((ValueError, TypeError)):
        process_data(None)

pyproject.toml Configuration

Complete Pytest Configuration

[tool.pytest.ini_options]
# Test discovery
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

# Output and behavior
addopts = [
    "-v",
    "--strict-markers",
    "--tb=short",
    "--cov=app",
    "--cov-report=term-missing",
    "--cov-report=html",
]

# Custom markers
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
    "unit: marks tests as unit tests",
    "asyncio: marks tests as async tests",
]

# Filter warnings as errors
filterwarnings = [
    "error",
    "ignore::DeprecationWarning:setuptools",  # Ignore specific warnings if needed
]

Coverage Configuration

[tool.coverage.run]
source = ["app"]
omit = [
    "*/tests/*",
    "*/migrations/*",
    "*/__init__.py",
    "*/__main__.py",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
    "class .*\\bProtocol\\):",
    "@(abc\\.)?abstractmethod",
]
min_coverage = 80

Makefile Integration

Test Targets

.PHONY: test test-unit test-integration test-coverage test-quick

# Run all tests
test:
	uv run pytest

# Unit tests only
test-unit:
	uv run pytest tests/unit/ -v

# Integration tests
test-integration:
	uv run pytest tests/integration/ -v

# Quick tests (skip slow)
test-quick:
	uv run pytest -m "not slow" -v

# Coverage report
test-coverage:
	uv run pytest --cov=app --cov-report=html --cov-report=term
	@echo "Coverage report: htmlcov/index.html"

# Watch mode (requires pytest-watch)
test-watch:
	uv run ptw tests/unit/

# Full test suite with all checks
check: test lint type-check
	@echo "All checks passed!"

Development Dependencies Installation

Installing with UV

# Core testing
uv add --dev pytest pytest-cov pytest-asyncio

# Mocking
uv add --dev pytest-mock

# Performance testing
uv add --dev pytest-benchmark

# Test data generation
uv add --dev faker factory-boy

# HTTP mocking
uv add --dev responses

# Watch mode (optional)
uv add --dev pytest-watch

# Parallel execution (optional)
uv add --dev pytest-xdist

Import Error Solutions

Issue: Tests Can't Import App Modules

Solution 1: Ensure init.py exists

tests/
├── __init__.py  # Important!
├── conftest.py
└── unit/
    ├── __init__.py
    └── test_*.py

Solution 2: Add to conftest.py

import sys
from pathlib import Path

# Add src directory to path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

Solution 3: Use pyproject.toml

[tool.pytest.ini_options]
pythonpath = ["src"]

Deleted File Handling

Skip Module-Level When Imports Fail

Move pytest.skip() BEFORE imports to prevent collection errors:

# tests/removed_feature_test.py
import pytest

pytest.skip("Module removed - feature deleted", allow_module_level=True)

from deleted_module import Something  # Won't be evaluated

Warnings as Errors Policy

Enforce Clean Test Output

Treat all warnings as errors in tests:

# Run tests with warnings as errors
uv run pytest -W error

# Run with specific warning filters
uv run pytest -W error::DeprecationWarning

Configuration in pyproject.toml

[tool.pytest.ini_options]
filterwarnings = ["error"]

Handle Expected Warnings

import warnings
import pytest


def test_deprecated_function():
    """Test deprecated function with expected warning."""
    with pytest.warns(DeprecationWarning):
        deprecated_function()


def test_suppress_specific_warning():
    """Test while ignoring specific warning."""
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", DeprecationWarning)
        function_that_warns()

User Deletion Handling

Test Database User Cleanup

When tests create users, ensure cleanup:

@pytest.fixture
def created_user(user_service):
    """Create test user with automatic cleanup."""
    user = user_service.create_user(
        username="testuser",
        email="test@example.com",
        password="secure"
    )

    yield user

    # Cleanup - delete after test
    user_service.delete_user(user.id)


def test_user_operations(created_user):
    """Test operations on created user."""
    assert created_user.id is not None
    # User automatically deleted after test

Handle User Onboarding Flow

@pytest.fixture
def onboarded_user(user_service):
    """Create and fully onboard test user."""
    user = user_service.create_user(
        username="newuser",
        email="new@example.com",
        password="secure"
    )

    # Complete onboarding
    user_service.set_profile(user.id, name="Test User")
    user_service.verify_email(user.id)

    yield user

    # Cleanup
    user_service.delete_user(user.id)

Common Testing Patterns

Arrange-Act-Assert

def test_create_user_success(user_service):
    """Test successful user creation."""
    # Arrange - Set up test data
    user_data = {
        "username": "testuser",
        "email": "test@example.com",
        "age": 25
    }

    # Act - Execute functionality
    user = user_service.create_user(user_data)

    # Assert - Verify results
    assert user is not None
    assert user.username == "testuser"
    assert user.email == "test@example.com"
    assert user.age == 25

Database Integration

@pytest.fixture(scope="module")
def test_database():
    """Provide test database for integration tests."""
    db = Database("test.db")
    db.migrate()
    yield db
    db.close()


def test_user_repository_integration(test_database):
    """Test user repository with real database."""
    repo = UserRepository(test_database)

    # Create
    user = repo.create(username="testuser", email="test@example.com")
    assert user.id is not None

    # Retrieve
    retrieved = repo.get(user.id)
    assert retrieved.username == "testuser"

    # Update
    retrieved.email = "updated@example.com"
    repo.update(retrieved)

    # Verify
    updated = repo.get(user.id)
    assert updated.email == "updated@example.com"

Test Markers and Organization

@pytest.mark.slow
def test_expensive_operation():
    """Long-running test - can be skipped."""
    pass


@pytest.mark.integration
def test_database_integration():
    """Integration test with external service."""
    pass


@pytest.mark.asyncio
@pytest.mark.slow
def test_async_integration():
    """Async integration test."""
    pass


# Run commands:
# uv run pytest -m "not slow"          # Skip slow tests
# uv run pytest -m integration         # Only integration
# uv run pytest -m "integration and not slow"  # Filter multiple

Edge Case Testing

Common Edge Cases

def test_edge_cases():
    """Test edge cases comprehensively."""
    calculator = Calculator()

    # Empty input
    assert calculator.sum([]) == 0

    # Single item
    assert calculator.sum([5]) == 5

    # Negative numbers
    assert calculator.sum([-1, -2, -3]) == -6

    # Mixed positive/negative
    assert calculator.sum([10, -5, 3]) == 8

    # Large numbers
    assert calculator.sum([10**10, 10**10]) == 2 * 10**10

    # Zero
    assert calculator.sum([0, 0, 0]) == 0

None Value Handling

def test_none_handling(service):
    """Test handling of None values."""
    # None input raises error
    with pytest.raises(ValueError):
        service.process(None)

    # None in list gets filtered
    result = service.process_list([1, None, 3])
    assert result == [1, 3]

Note: For general testing principles and strategies not specific to Python, see the testing-workflow skill.