pytest

Fast, scalable Python testing with fixtures, parametrization, and framework integration. Fixture system provides dependency injection and setup/teardown with function, class, module, and session scopes Parametrization enables data-driven tests; markers organize tests by category (unit, integration, slow, custom) Built-in support for FastAPI, Django, and Flask with async/await testing via pytest-asyncio Rich assertion introspection, mocking via pytest-mock, coverage reporting, and parallel execution with pytest-xdist

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill pytest
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

pytest - Professional Python Testing

Overview

pytest is the industry-standard Python testing framework, offering powerful features like fixtures, parametrization, markers, plugins, and seamless integration with FastAPI, Django, and Flask. It provides a simple, scalable approach to testing from unit tests to complex integration scenarios.

Key Features:

  • Fixture system for dependency injection
  • Parametrization for data-driven tests
  • Rich assertion introspection (no need for self.assertEqual)
  • Plugin ecosystem (pytest-cov, pytest-asyncio, pytest-mock, pytest-django)
  • Async/await support
  • Parallel test execution with pytest-xdist
  • Test discovery and organization
  • Detailed failure reporting

Installation:

# Basic pytest

pip install pytest

# With common plugins

pip install pytest pytest-cov pytest-asyncio pytest-mock

# For FastAPI testing

pip install pytest httpx pytest-asyncio

# For Django testing

pip install pytest pytest-django

# For async databases

pip install pytest-asyncio aiosqlite

Basic Testing Patterns

1. Simple Test Functions

# test_math.py

def add(a, b):

    return a + b

def test_add():

    assert add(2, 3) == 5

    assert add(-1, 1) == 0

    assert add(0, 0) == 0

def test_add_negative():

    assert add(-2, -3) == -5

Run tests:

# Discover and run all tests

pytest

# Verbose output

pytest -v

# Show print statements

pytest -s

# Run specific test file

pytest test_math.py

# Run specific test function

pytest test_math.py::test_add

2. Test Classes for Organization

# test_calculator.py

class Calculator:

    def add(self, a, b):

        return a + b

    def multiply(self, a, b):

        return a * b

class TestCalculator:

    def test_add(self):

        calc = Calculator()

        assert calc.add(2, 3) == 5

    def test_multiply(self):

        calc = Calculator()

        assert calc.multiply(4, 5) == 20

    def test_add_negative(self):

        calc = Calculator()

        assert calc.add(-1, -1) == -2

3. Assertions and Expected Failures

import pytest

# Test exception raising

def divide(a, b):

    if b == 0:

        raise ValueError("Cannot divide by zero")

    return a / b

def test_divide_by_zero():

    with pytest.raises(ValueError, match="Cannot divide by zero"):

        divide(10, 0)

def test_divide_success():

    assert divide(10, 2) == 5.0

# Test approximate equality

def test_float_comparison():

    assert 0.1 + 0.2 == pytest.approx(0.3)

# Test containment

def test_list_contains():

    result = [1, 2, 3, 4]

    assert 3 in result

    assert len(result) == 4

Fixtures - Dependency Injection

Basic Fixtures

# conftest.py

import pytest

@pytest.fixture

def sample_data():

    """Provide sample data for tests."""

    return {"name": "Alice", "age": 30, "email": "alice@example.com"}

@pytest.fixture

def empty_list():

    """Provide an empty list."""

    return []

# test_fixtures.py

def test_sample_data(sample_data):

    assert sample_data["name"] == "Alice"

    assert sample_data["age"] == 30

def test_empty_list(empty_list):

    empty_list.append(1)

    assert len(empty_list) == 1

Fixture Scopes

import pytest

# Function scope (default) - runs for each test

@pytest.fixture(scope="function")

def user():

    return {"id": 1, "name": "Alice"}

# Class scope - runs once per test class

@pytest.fixture(scope="class")

def database():

    db = setup_database()

    yield db

    db.close()

# Module scope - runs once per test module

@pytest.fixture(scope="module")

def api_client():

    client = APIClient()

    yield client

    client.shutdown()

# Session scope - runs once for entire test session

@pytest.fixture(scope="session")

def app_config():

    return load_config()

Fixture Setup and Teardown

import pytest

import tempfile

import shutil

@pytest.fixture

def temp_directory():

    """Create a temporary directory for test."""

    temp_dir = tempfile.mkdtemp()

    print(f"

Setup: Created {temp_dir}")

    yield temp_dir  # Provide directory to test

    # Teardown: cleanup after test

    shutil.rmtree(temp_dir)

    print(f"

Teardown: Removed {temp_dir}")

def test_file_creation(temp_directory):

    file_path = f"{temp_directory}/test.txt"

    with open(file_path, "w") as f:

        f.write("test content")

    assert os.path.exists(file_path)

Fixture Dependencies

import pytest

@pytest.fixture

def database_connection():

    """Database connection."""

    conn = connect_to_db()

    yield conn

    conn.close()

@pytest.fixture

def database_session(database_connection):

    """Database session depends on connection."""

    session = create_session(database_connection)

    yield session

    session.rollback()

    session.close()

@pytest.fixture

def user_repository(database_session):

    """User repository depends on session."""

    return UserRepository(database_session)

def test_create_user(user_repository):

    user = user_repository.create(name="Alice", email="alice@example.com")

    assert user.name == "Alice"

Parametrization - Data-Driven Testing

Basic Parametrization

import pytest

@pytest.mark.parametrize("a,b,expected", [

    (2, 3, 5),

    (5, 7, 12),

    (-1, 1, 0),

    (0, 0, 0),

    (100, 200, 300),

])

def test_add_parametrized(a, b, expected):

    assert add(a, b) == expected

Multiple Parameters

@pytest.mark.parametrize("operation,a,b,expected", [

    ("add", 2, 3, 5),

    ("subtract", 10, 5, 5),

    ("multiply", 4, 5, 20),

    ("divide", 10, 2, 5),

])

def test_calculator_operations(operation, a, b, expected):

    calc = Calculator()

    result = getattr(calc, operation)(a, b)

    assert result == expected

Parametrize with IDs

@pytest.mark.parametrize("input_data,expected", [

    pytest.param({"name": "Alice"}, "Alice", id="valid_name"),

    pytest.param({"name": ""}, None, id="empty_name"),

    pytest.param({}, None, id="missing_name"),

], ids=lambda x: x if isinstance(x, str) else None)

def test_extract_name(input_data, expected):

    result = extract_name(input_data)

    assert result == expected

Indirect Parametrization (Fixtures)

@pytest.fixture

def user_data(request):

    """Create user based on parameter."""

    return {"name": request.param, "email": f"{request.param}@example.com"}

@pytest.mark.parametrize("user_data", ["Alice", "Bob", "Charlie"], indirect=True)

def test_user_creation(user_data):

    assert "@example.com" in user_data["email"]

Test Markers

Built-in Markers

import pytest

# Skip test

@pytest.mark.skip(reason="Not implemented yet")

def test_future_feature():

    pass

# Skip conditionally

@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test")

def test_unix_specific():

    pass

# Expected failure

@pytest.mark.xfail(reason="Known bug #123")

def test_known_bug():

    assert False

# Slow test marker

@pytest.mark.slow

def test_expensive_operation():

    time.sleep(5)

    assert True

Custom Markers

# pytest.ini

[pytest]

markers =

    slow: marks tests as slow (deselect with '-m "not slow"')

    integration: marks tests as integration tests

    unit: marks tests as unit tests

    smoke: marks tests as smoke tests

# test_custom_markers.py

import pytest

@pytest.mark.unit

def test_fast_unit():

    assert True

@pytest.mark.integration

@pytest.mark.slow

def test_slow_integration():

    # Integration test with database

    pass

@pytest.mark.smoke

def test_critical_path():

    # Smoke test for critical functionality

    pass

Run tests by marker:

# Run only unit tests

pytest -m unit

# Run all except slow tests

pytest -m "not slow"

# Run integration tests

pytest -m integration

# Run unit AND integration

pytest -m "unit or integration"

# Run smoke tests only

pytest -m smoke

FastAPI Testing

Basic FastAPI Test Setup

# app/main.py

from fastapi import FastAPI, HTTPException

from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):

    name: str

    price: float

@app.get("/")

def read_root():

    return {"message": "Hello World"}

@app.get("/items/{item_id}")

def read_item(item_id: int):

    if item_id == 0:

        raise HTTPException(status_code=404, detail="Item not found")

    return {"item_id": item_id, "name": f"Item {item_id}"}

@app.post("/items")

def create_item(item: Item):

    return {"name": item.name, "price": item.price, "id": 123}

FastAPI Test Client

# conftest.py

import pytest

from fastapi.testclient import TestClient

from app.main import app

@pytest.fixture

def client():

    """FastAPI test client."""

    return TestClient(app)

# test_api.py

def test_read_root(client):

    response = client.get("/")

    assert response.status_code == 200

    assert response.json() == {"message": "Hello World"}

def test_read_item(client):

    response = client.get("/items/1")

    assert response.status_code == 200

    assert response.json() == {"item_id": 1, "name": "Item 1"}

def test_read_item_not_found(client):

    response = client.get("/items/0")

    assert response.status_code == 404

    assert response.json() == {"detail": "Item not found"}

def test_create_item(client):

    response = client.post(

        "/items",

        json={"name": "Widget", "price": 9.99}

    )

    assert response.status_code == 200

    data = response.json()

    assert data["name"] == "Widget"

    assert data["price"] == 9.99

    assert "id" in data

Async FastAPI Testing

# conftest.py

import pytest

from httpx import AsyncClient

from app.main import app

@pytest.fixture

async def async_client():

    """Async test client for FastAPI."""

    async with AsyncClient(app=app, base_url="http://test") as client:

        yield client

# test_async_api.py

import pytest

@pytest.mark.asyncio

async def test_read_root_async(async_client):

    response = await async_client.get("/")

    assert response.status_code == 200

    assert response.json() == {"message": "Hello World"}

@pytest.mark.asyncio

async def test_create_item_async(async_client):

    response = await async_client.post(

        "/items",

        json={"name": "Gadget", "price": 19.99}

    )

    assert response.status_code == 200

    assert response.json()["name"] == "Gadget"

FastAPI with Database Testing

# conftest.py

import pytest

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker

from app.database import Base, get_db

from app.main import app

# Test database

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})

TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="function")

def test_db():

    """Create test database."""

    Base.metadata.create_all(bind=engine)

    yield

    Base.metadata.drop_all(bind=engine)

@pytest.fixture

def client(test_db):

    """Override database dependency."""

    def override_get_db():

        try:

            db = TestingSessionLocal()

            yield db

        finally:

            db.close()

    app.dependency_overrides[get_db] = override_get_db

    with TestClient(app) as test_client:

        yield test_client

    app.dependency_overrides.clear()

# test_users.py

def test_create_user(client):

    response = client.post(

        "/users",

        json={"email": "test@example.com", "password": "secret"}

    )

    assert response.status_code == 200

    assert response.json()["email"] == "test@example.com"

def test_read_users(client):

    # Create user first

    client.post("/users", json={"email": "user1@example.com", "password": "pass1"})

    client.post("/users", json={"email": "user2@example.com", "password": "pass2"})

    # Read users

    response = client.get("/users")

    assert response.status_code == 200

    assert len(response.json()) == 2

Django Testing

Django pytest Configuration

# pytest.ini

[pytest]

DJANGO_SETTINGS_MODULE = myproject.settings

python_files = tests.py test_*.py *_tests.py

# conftest.py

import pytest

from django.conf import settings

@pytest.fixture(scope='session')

def django_db_setup():

    settings.DATABASES['default'] = {

        'ENGINE': 'django.db.backends.sqlite3',

        'NAME': ':memory:',

    }

Django Model Testing

# models.py

from django.db import models

class User(models.Model):

    email = models.EmailField(unique=True)

    name = models.CharField(max_length=100)

    is_active = models.BooleanField(default=True)

# test_models.py

import pytest

from myapp.models import User

@pytest.mark.django_db

def test_create_user():

    user = User.objects.create(

        email="test@example.com",

        name="Test User"

    )

    assert user.email == "test@example.com"

    assert user.is_active is True

@pytest.mark.django_db

def test_user_unique_email():

    User.objects.create(email="test@example.com", name="User 1")

    with pytest.raises(Exception):  # IntegrityError

        User.objects.create(email="test@example.com", name="User 2")

Django View Testing

# views.py

from django.http import JsonResponse

from django.views import View

class UserListView(View):

    def get(self, request):

        users = User.objects.all()

        return JsonResponse({

            "users": list(users.values("id", "email", "name"))

        })

# test_views.py

import pytest

from django.test import Client

from myapp.models import User

@pytest.fixture

def client():

    return Client()

@pytest.mark.django_db

def test_user_list_view(client):

    # Create test data

    User.objects.create(email="user1@example.com", name="User 1")

    User.objects.create(email="user2@example.com", name="User 2")

    # Test view

    response = client.get("/users/")

    assert response.status_code == 200

    data = response.json()

    assert len(data["users"]) == 2

Django REST Framework Testing

# serializers.py

from rest_framework import serializers

from myapp.models import User

class UserSerializer(serializers.ModelSerializer):

    class Meta:

        model = User

        fields = ['id', 'email', 'name', 'is_active']

# views.py

from rest_framework import viewsets

from myapp.models import User

from myapp.serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):

    queryset = User.objects.all()

    serializer_class = UserSerializer

# test_api.py

import pytest

from rest_framework.test import APIClient

from myapp.models import User

@pytest.fixture

def api_client():

    return APIClient()

@pytest.mark.django_db

def test_list_users(api_client):

    User.objects.create(email="user1@example.com", name="User 1")

    User.objects.create(email="user2@example.com", name="User 2")

    response = api_client.get("/api/users/")

    assert response.status_code == 200

    assert len(response.data) == 2

@pytest.mark.django_db

def test_create_user(api_client):

    data = {"email": "new@example.com", "name": "New User"}

    response = api_client.post("/api/users/", data)

    assert response.status_code == 201

    assert User.objects.filter(email="new@example.com").exists()

Mocking and Patching

pytest-mock (pytest.fixture.mocker)

# Install: pip install pytest-mock

# service.py

import requests

def get_user_data(user_id):

    response = requests.get(f"https://api.example.com/users/{user_id}")

    return response.json()

# test_service.py

def test_get_user_data(mocker):

    # Mock requests.get

    mock_response = mocker.Mock()

    mock_response.json.return_value = {"id": 1, "name": "Alice"}

    mocker.patch("requests.get", return_value=mock_response)

    result = get_user_data(1)

    assert result["name"] == "Alice"

Mocking Class Methods

class UserService:

    def get_user(self, user_id):

        # Database call

        return database.fetch_user(user_id)

    def get_user_name(self, user_id):

        user = self.get_user(user_id)

        return user["name"]

def test_get_user_name(mocker):

    service = UserService()

    # Mock the get_user method

    mocker.patch.object(

        service,

        "get_user",

        return_value={"id": 1, "name": "Alice"}

    )

    result = service.get_user_name(1)

    assert result == "Alice"

Mocking with Side Effects

def test_retry_on_failure(mocker):

    # First call fails, second succeeds

    mock_api = mocker.patch("requests.get")

    mock_api.side_effect = [

        requests.exceptions.Timeout(),  # First call

        mocker.Mock(json=lambda: {"status": "ok"})  # Second call

    ]

    result = api_call_with_retry()

    assert result["status"] == "ok"

    assert mock_api.call_count == 2

Spy on Calls

def test_function_called_correctly(mocker):

    spy = mocker.spy(module, "function_name")

    # Call code that uses the function

    module.run_workflow()

    # Verify it was called

    assert spy.call_count == 1

    spy.assert_called_once_with(arg1="value", arg2=42)

Coverage and Reporting

pytest-cov Configuration

# Install

pip install pytest-cov

# Run with coverage

pytest --cov=app --cov-report=html --cov-report=term

# Generate coverage report

pytest --cov=app --cov-report=term-missing

# Coverage with minimum threshold

pytest --cov=app --cov-fail-under=80

pytest.ini Coverage Configuration

# pytest.ini

[pytest]

addopts =

    --cov=app

    --cov-report=html

    --cov-report=term-missing

    --cov-fail-under=80

    -v

testpaths = tests

python_files = test_*.py

python_classes = Test*

python_functions = test_*

Coverage Reports

# HTML report (opens in browser)

pytest --cov=app --cov-report=html

open htmlcov/index.html

# Terminal report with missing lines

pytest --cov=app --cov-report=term-missing

# XML report (for CI/CD)

pytest --cov=app --cov-report=xml

# JSON report

pytest --cov=app --cov-report=json

Async Testing

pytest-asyncio

# Install: pip install pytest-asyncio

# conftest.py

import pytest

# Enable asyncio mode

pytest_plugins = ('pytest_asyncio',)

# async_service.py

import asyncio

import aiohttp

async def fetch_data(url):

    async with aiohttp.ClientSession() as session:

        async with session.get(url) as response:

            return await response.json()

# test_async_service.py

import pytest

@pytest.mark.asyncio

async def test_fetch_data(mocker):

    # Mock aiohttp response

    mock_response = mocker.AsyncMock()

    mock_response.json.return_value = {"data": "test"}

    mock_session = mocker.AsyncMock()

    mock_session.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response

    mocker.patch("aiohttp.ClientSession", return_value=mock_session)

    result = await fetch_data("https://api.example.com/data")

    assert result["data"] == "test"

Async Fixtures

@pytest.fixture

async def async_db_session():

    """Async database session."""

    async with async_engine.begin() as conn:

        await conn.run_sync(Base.metadata.create_all)

    async with AsyncSession(async_engine) as session:

        yield session

    async with async_engine.begin() as conn:

        await conn.run_sync(Base.metadata.drop_all)

@pytest.mark.asyncio

async def test_create_user_async(async_db_session):

    user = User(email="test@example.com", name="Test")

    async_db_session.add(user)

    await async_db_session.commit()

    result = await async_db_session.execute(

        select(User).where(User.email == "test@example.com")

    )

    assert result.scalar_one().name == "Test"

Local pytest Profiles (Your Repos)

Common settings from your projects' pyproject.toml:

  • asyncio_mode = "auto" (default in mcp-browser, mcp-memory, claude-mpm, edgar)
  • addopts includes --strict-markers and --strict-config for CI consistency
  • Coverage flags: --cov=<package>, --cov-report=term-missing, --cov-report=xml
  • Selective ignores (mcp-vector-search): --ignore=tests/manual, --ignore=tests/e2e
  • pythonpath = ["src"] for editable import resolution (mcp-ticketer)

Typical markers:

  • unit, integration, e2e
  • slow, benchmark, performance
  • requires_api (edgar)

Reference: see pyproject.toml in claude-mpm, edgar, mcp-vector-search, mcp-ticketer, and kuzu-memory for full lists.

Best Practices

1. Test Organization

project/

├── app/

│   ├── __init__.py

│   ├── main.py

│   ├── models.py

│   └── services.py

├── tests/

│   ├── __init__.py

│   ├── conftest.py          # Shared fixtures

│   ├── test_models.py       # Model tests

│   ├── test_services.py     # Service tests

│   ├── test_api.py          # API tests

│   └── integration/

│       ├── __init__.py

│       └── test_workflows.py

└── pytest.ini

2. Naming Conventions

# ✅ GOOD: Clear test names

def test_user_creation_with_valid_email():

    pass

def test_user_creation_raises_error_for_duplicate_email():

    pass

# ❌ BAD: Vague names

def test_user1():

    pass

def test_case2():

    pass

3. Arrange-Act-Assert Pattern

def test_user_service_creates_user():

    # Arrange: Setup test data and dependencies

    service = UserService(database=mock_db)

    user_data = {"email": "test@example.com", "name": "Test"}

    # Act: Perform the action being tested

    result = service.create_user(user_data)

    # Assert: Verify the outcome

    assert result.email == "test@example.com"

    assert result.id is not None

4. Use Fixtures for Common Setup

# ❌ BAD: Repeated setup

def test_user_creation():

    db = setup_database()

    user = create_user(db)

    assert user.id is not None

    db.close()

def test_user_deletion():

    db = setup_database()

    user = create_user(db)

    delete_user(db, user.id)

    db.close()

# ✅ GOOD: Fixture-based setup

@pytest.fixture

def db():

    database = setup_database()

    yield database

    database.close()

@pytest.fixture

def user(db):

    return create_user(db)

def test_user_creation(user):

    assert user.id is not None

def test_user_deletion(db, user):

    delete_user(db, user.id)

    assert not user_exists(db, user.id)

5. Parametrize Similar Tests

# ❌ BAD: Duplicate test code

def test_add_positive():

    assert add(2, 3) == 5

def test_add_negative():

    assert add(-2, -3) == -5

def test_add_zero():

    assert add(0, 0) == 0

# ✅ GOOD: Parametrized tests

@pytest.mark.parametrize("a,b,expected", [

    (2, 3, 5),

    (-2, -3, -5),

    (0, 0, 0),

])

def test_add(a, b, expected):

    assert add(a, b) == expected

6. Test One Thing Per Test

# ❌ BAD: Testing multiple things

def test_user_workflow():

    user = create_user()

    assert user.id is not None

    updated = update_user(user.id, name="New Name")

    assert updated.name == "New Name"

    deleted = delete_user(user.id)

    assert deleted is True

# ✅ GOOD: Separate tests

def test_user_creation():

    user = create_user()

    assert user.id is not None

def test_user_update():

    user = create_user()

    updated = update_user(user.id, name="New Name")

    assert updated.name == "New Name"

def test_user_deletion():

    user = create_user()

    result = delete_user(user.id)

    assert result is True

7. Use Markers for Test Organization

@pytest.mark.unit

def test_pure_function():

    pass

@pytest.mark.integration

@pytest.mark.slow

def test_database_integration():

    pass

@pytest.mark.smoke

def test_critical_path():

    pass

8. Mock External Dependencies

# ✅ GOOD: Mock external API

def test_fetch_user_data(mocker):

    mocker.patch("requests.get", return_value=mock_response)

    result = fetch_user_data(user_id=1)

    assert result["name"] == "Alice"

# ❌ BAD: Real API call in test

def test_fetch_user_data():

    result = fetch_user_data(user_id=1)  # Real HTTP request!

    assert result["name"] == "Alice"

Common Pitfalls

❌ Anti-Pattern 1: Test Depends on Execution Order

# WRONG: Tests should be independent

class TestUserWorkflow:

    user_id = None

    def test_create_user(self):

        user = create_user()

        TestUserWorkflow.user_id = user.id

    def test_update_user(self):

        # Fails if test_create_user didn't run first!

        update_user(TestUserWorkflow.user_id, name="New")

Correct:

@pytest.fixture

def created_user():

    return create_user()

def test_create_user(created_user):

    assert created_user.id is not None

def test_update_user(created_user):

    update_user(created_user.id, name="New")

❌ Anti-Pattern 2: Not Cleaning Up Resources

# WRONG: Database not cleaned up

def test_user_creation():

    db = setup_database()

    user = create_user(db)

    assert user.id is not None

    # Database connection not closed!

Correct:

@pytest.fixture

def db():

    database = setup_database()

    yield database

    database.close()  # Cleanup

❌ Anti-Pattern 3: Testing Implementation Details

# WRONG: Testing internal implementation

def test_user_service_uses_cache():

    service = UserService()

    service.get_user(1)

    assert service._cache.has_key(1)  # Testing internal cache!

Correct:

# Test behavior, not implementation

def test_user_service_returns_user():

    service = UserService()

    user = service.get_user(1)

    assert user.id == 1

❌ Anti-Pattern 4: Not Using pytest Features

# WRONG: Using unittest assertions

import unittest

def test_addition():

    result = add(2, 3)

    unittest.TestCase().assertEqual(result, 5)

Correct:

# Use pytest's rich assertions

def test_addition():

    assert add(2, 3) == 5

❌ Anti-Pattern 5: Overly Complex Fixtures

# WRONG: Fixture does too much

@pytest.fixture

def everything():

    db = setup_db()

    user = create_user(db)

    session = login(user)

    cache = setup_cache()

    # ... too many things!

    return {"db": db, "user": user, "session": session, "cache": cache}

Correct:

# Separate, composable fixtures

@pytest.fixture

def db():

    return setup_db()

@pytest.fixture

def user(db):

    return create_user(db)

@pytest.fixture

def session(user):

    return login(user)

Quick Reference

Common Commands

# Run all tests

pytest

# Verbose output

pytest -v

# Show print statements

pytest -s

# Run specific file

pytest tests/test_api.py

# Run specific test

pytest tests/test_api.py::test_create_user

# Run by marker

pytest -m unit

pytest -m "not slow"

# Run with coverage

pytest --cov=app --cov-report=html

# Parallel execution

pytest -n auto  # Requires pytest-xdist

# Stop on first failure

pytest -x

# Show local variables on failure

pytest -l

# Run last failed tests

pytest --lf

# Run failed tests first

pytest --ff

pytest.ini Template

[pytest]

# Minimum pytest version

minversion = 7.0

# Test discovery patterns

python_files = test_*.py *_test.py

python_classes = Test*

python_functions = test_*

# Test paths

testpaths = tests

# Command line options

addopts =

    -v

    --strict-markers

    --cov=app

    --cov-report=html

    --cov-report=term-missing

    --cov-fail-under=80

# Markers

markers =

    unit: Unit tests

    integration: Integration tests

    slow: Slow-running tests

    smoke: Smoke tests for critical paths

# Django settings (if using Django)

DJANGO_SETTINGS_MODULE = myproject.settings

# Asyncio mode

asyncio_mode = auto

conftest.py Template

# conftest.py

import pytest

from fastapi.testclient import TestClient

from app.main import app

# FastAPI client fixture

@pytest.fixture

def client():

    return TestClient(app)

# Database fixture

@pytest.fixture(scope="function")

def db():

    database = setup_test_database()

    yield database

    database.close()

# Mock user fixture

@pytest.fixture

def mock_user():

    return {"id": 1, "email": "test@example.com", "name": "Test User"}

# Custom pytest configuration

def pytest_configure(config):

    config.addinivalue_line("markers", "api: API tests")

    config.addinivalue_line("markers", "db: Database tests")

Resources

Related Skills

When using pytest, consider these complementary skills:

  • fastapi-local-dev: FastAPI development server patterns and test fixtures
  • test-driven-development: Complete TDD workflow (RED/GREEN/REFACTOR cycle)
  • systematic-debugging: Root cause investigation for failing tests

Quick TDD Workflow Reference (Inlined for Standalone Use)

RED → GREEN → REFACTOR Cycle:

-

RED Phase: Write Failing Test

def test_should_authenticate_user_when_credentials_valid():

    # Test that describes desired behavior

    user = User(username='alice', password='secret123')

    result = authenticate(user)

    assert result.is_authenticated is True

    # This test will fail because authenticate() doesn't exist yet

-

GREEN Phase: Make It Pass

def authenticate(user):

    # Minimum code to pass the test

    if user.username == 'alice' and user.password == 'secret123':

        return AuthResult(is_authenticated=True)

    return AuthResult(is_authenticated=False)

-

REFACTOR Phase: Improve Code

def authenticate(user):

    # Clean up while keeping tests green

    hashed_password = hash_password(user.password)

    stored_user = database.get_user(user.username)

    return AuthResult(

        is_authenticated=(stored_user.password_hash == hashed_password)

    )

Test Structure: Arrange-Act-Assert (AAA)

def test_user_creation():

    # Arrange: Set up test data

    user_data = {'username': 'alice', 'email': 'alice@example.com'}

    # Act: Perform the action

    user = create_user(user_data)

    # Assert: Verify outcome

    assert user.username == 'alice'

    assert user.email == 'alice@example.com'

Quick Debugging Reference (Inlined for Standalone Use)

Phase 1: Root Cause Investigation

  • Read error messages completely (stack traces, line numbers)
  • Reproduce consistently (document exact steps)
  • Check recent changes (git log, git diff)
  • Understand what changed and why it might cause failure

Phase 2: Isolate the Problem

# Use pytest's built-in debugging

pytest tests/test_auth.py -vv --pdb  # Drop into debugger on failure

pytest tests/test_auth.py -x         # Stop on first failure

pytest tests/test_auth.py -k "auth"  # Run only auth-related tests

# Add strategic print/logging

def test_complex_workflow():

    user = create_user({'username': 'test'})

    print(f"DEBUG: Created user {user.id}")  # Visible with pytest -s

    result = process_user(user)

    print(f"DEBUG: Result status {result.status}")

    assert result.success

Phase 3: Fix Root Cause

  • Fix the underlying problem, not symptoms
  • Add regression test to prevent recurrence
  • Verify fix doesn't break other tests

Phase 4: Verify Solution

# Run full test suite

pytest

# Run with coverage

pytest --cov=src --cov-report=html

# Verify specific test patterns

pytest -k "auth or login" -v

[Full TDD and debugging workflows available in respective skills if deployed together]

pytest Version Compatibility: This skill covers pytest 7.0+ and reflects current best practices for Python testing in 2025.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card