Pytest

by bobmatnyc718070a7d622MIT77 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

pytest - Python's most powerful testing framework with fixtures, parametrization, plugins, and framework integration for FastAPI, Django, Flask

Instructions onlySoftware Development
AI-generated overview

Reference guide for writing and running Python tests with pytest, including fixtures, parametrization, mocking and web framework integration.

What it does
This skill is an instructional reference for pytest, Python's testing framework. It covers test functions and classes, assertions, fixtures with scopes and teardown, parametrization, markers, mocking and patching, coverage reporting, and async tests. It also shows how to test FastAPI, Django and Django REST Framework applications, including test clients and database fixtures. It produces guidance and code examples rather than runnable artifacts.
When to use it
Use it when writing or organizing Python unit, integration or API tests, adopting a TDD workflow, testing async code or databases, or mocking external dependencies. It is also relevant when setting up pytest for FastAPI or Django projects.
Requirements
No scripts are shipped; the skill is instructions and code examples only. Following it requires Python and pytest, plus optional plugins such as pytest-cov, pytest-asyncio, pytest-mock, pytest-django and pytest-xdist, and framework packages like FastAPI, httpx, Django and SQLAlchemy depending on the target project.

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:

bash
# Basic pytestpip install pytest
# With common pluginspip install pytest pytest-cov pytest-asyncio pytest-mock
# For FastAPI testingpip install pytest httpx pytest-asyncio
# For Django testingpip install pytest pytest-django
# For async databasespip install pytest-asyncio aiosqlite

Basic Testing Patterns

1. Simple Test Functions

python
# test_math.pydef 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:

bash
# Discover and run all testspytest
# Verbose outputpytest -v
# Show print statementspytest -s
# Run specific test filepytest test_math.py
# Run specific test functionpytest test_math.py::test_add

2. Test Classes for Organization

python
# test_calculator.pyclass 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

python
import pytest
# Test exception raisingdef 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 equalitydef test_float_comparison():    assert 0.1 + 0.2 == pytest.approx(0.3)
# Test containmentdef test_list_contains():    result = [1, 2, 3, 4]    assert 3 in result    assert len(result) == 4

Fixtures - Dependency Injection

Basic Fixtures

python
# conftest.pyimport pytest
@pytest.fixturedef sample_data():    """Provide sample data for tests."""    return {"name": "Alice", "age": 30, "email": "[email protected]"}
@pytest.fixturedef empty_list():    """Provide an empty list."""    return []
# test_fixtures.pydef 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

python
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

python
import pytestimport tempfileimport shutil
@pytest.fixturedef 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

python
import pytest
@pytest.fixturedef database_connection():    """Database connection."""    conn = connect_to_db()    yield conn    conn.close()
@pytest.fixturedef database_session(database_connection):    """Database session depends on connection."""    session = create_session(database_connection)    yield session    session.rollback()    session.close()
@pytest.fixturedef 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="[email protected]")    assert user.name == "Alice"

Parametrization - Data-Driven Testing

Basic Parametrization

python
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

python
@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

python
@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)

python
@pytest.fixturedef 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

python
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.slowdef test_expensive_operation():    time.sleep(5)    assert True

Custom Markers

python
# 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.pyimport pytest
@pytest.mark.unitdef test_fast_unit():    assert True
@pytest.mark.integration@pytest.mark.slowdef test_slow_integration():    # Integration test with database    pass
@pytest.mark.smokedef test_critical_path():    # Smoke test for critical functionality    pass

Run tests by marker:

bash
# Run only unit testspytest -m unit
# Run all except slow testspytest -m "not slow"
# Run integration testspytest -m integration
# Run unit AND integrationpytest -m "unit or integration"
# Run smoke tests onlypytest -m smoke

FastAPI Testing

Basic FastAPI Test Setup

python
# app/main.pyfrom fastapi import FastAPI, HTTPExceptionfrom 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

python
# conftest.pyimport pytestfrom fastapi.testclient import TestClientfrom app.main import app
@pytest.fixturedef client():    """FastAPI test client."""    return TestClient(app)
# test_api.pydef 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

python
# conftest.pyimport pytestfrom httpx import AsyncClientfrom app.main import app
@pytest.fixtureasync def async_client():    """Async test client for FastAPI."""    async with AsyncClient(app=app, base_url="http://test") as client:        yield client
# test_async_api.pyimport pytest
@pytest.mark.asyncioasync 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.asyncioasync 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

python
# conftest.pyimport pytestfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmakerfrom app.database import Base, get_dbfrom app.main import app
# Test databaseSQLALCHEMY_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.fixturedef 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.pydef test_create_user(client):    response = client.post(        "/users",        json={"email": "[email protected]", "password": "secret"}  # pragma: allowlist secret    )    assert response.status_code == 200    assert response.json()["email"] == "[email protected]"
def test_read_users(client):    # Create user first    client.post("/users", json={"email": "[email protected]", "password": "pass1"})  # pragma: allowlist secret    client.post("/users", json={"email": "[email protected]", "password": "pass2"})  # pragma: allowlist secret
    # Read users    response = client.get("/users")    assert response.status_code == 200    assert len(response.json()) == 2

Django Testing

Django pytest Configuration

python
# pytest.ini[pytest]DJANGO_SETTINGS_MODULE = myproject.settingspython_files = tests.py test_*.py *_tests.py
# conftest.pyimport pytestfrom 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

python
# models.pyfrom 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.pyimport pytestfrom myapp.models import User
@pytest.mark.django_dbdef test_create_user():    user = User.objects.create(        email="[email protected]",        name="Test User"    )    assert user.email == "[email protected]"    assert user.is_active is True
@pytest.mark.django_dbdef test_user_unique_email():    User.objects.create(email="[email protected]", name="User 1")
    with pytest.raises(Exception):  # IntegrityError        User.objects.create(email="[email protected]", name="User 2")

Django View Testing

python
# views.pyfrom django.http import JsonResponsefrom 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.pyimport pytestfrom django.test import Clientfrom myapp.models import User
@pytest.fixturedef client():    return Client()
@pytest.mark.django_dbdef test_user_list_view(client):    # Create test data    User.objects.create(email="[email protected]", name="User 1")    User.objects.create(email="[email protected]", 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

python
# serializers.pyfrom rest_framework import serializersfrom myapp.models import User
class UserSerializer(serializers.ModelSerializer):    class Meta:        model = User        fields = ['id', 'email', 'name', 'is_active']
# views.pyfrom rest_framework import viewsetsfrom myapp.models import Userfrom myapp.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):    queryset = User.objects.all()    serializer_class = UserSerializer
# test_api.pyimport pytestfrom rest_framework.test import APIClientfrom myapp.models import User
@pytest.fixturedef api_client():    return APIClient()
@pytest.mark.django_dbdef test_list_users(api_client):    User.objects.create(email="[email protected]", name="User 1")    User.objects.create(email="[email protected]", name="User 2")
    response = api_client.get("/api/users/")    assert response.status_code == 200    assert len(response.data) == 2
@pytest.mark.django_dbdef test_create_user(api_client):    data = {"email": "[email protected]", "name": "New User"}    response = api_client.post("/api/users/", data)
    assert response.status_code == 201    assert User.objects.filter(email="[email protected]").exists()

Mocking and Patching

pytest-mock (pytest.fixture.mocker)

python
# Install: pip install pytest-mock
# service.pyimport requests
def get_user_data(user_id):    response = requests.get(f"https://api.example.com/users/{user_id}")    return response.json()
# test_service.pydef 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

python
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

python
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

python
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

bash
# Installpip install pytest-cov
# Run with coveragepytest --cov=app --cov-report=html --cov-report=term
# Generate coverage reportpytest --cov=app --cov-report=term-missing
# Coverage with minimum thresholdpytest --cov=app --cov-fail-under=80

pytest.ini Coverage Configuration

ini
# pytest.ini[pytest]addopts =    --cov=app    --cov-report=html    --cov-report=term-missing    --cov-fail-under=80    -vtestpaths = testspython_files = test_*.pypython_classes = Test*python_functions = test_*

Coverage Reports

bash
# HTML report (opens in browser)pytest --cov=app --cov-report=htmlopen htmlcov/index.html
# Terminal report with missing linespytest --cov=app --cov-report=term-missing
# XML report (for CI/CD)pytest --cov=app --cov-report=xml
# JSON reportpytest --cov=app --cov-report=json

Async Testing

pytest-asyncio

python
# Install: pip install pytest-asyncio
# conftest.pyimport pytest
# Enable asyncio modepytest_plugins = ('pytest_asyncio',)
# async_service.pyimport asyncioimport 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.pyimport pytest
@pytest.mark.asyncioasync 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

python
@pytest.fixtureasync 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.asyncioasync def test_create_user_async(async_db_session):    user = User(email="[email protected]", name="Test")    async_db_session.add(user)    await async_db_session.commit()
    result = await async_db_session.execute(        select(User).where(User.email == "[email protected]")    )    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

python
# ✅ GOOD: Clear test namesdef test_user_creation_with_valid_email():    pass
def test_user_creation_raises_error_for_duplicate_email():    pass
# ❌ BAD: Vague namesdef test_user1():    pass
def test_case2():    pass

3. Arrange-Act-Assert Pattern

python
def test_user_service_creates_user():    # Arrange: Setup test data and dependencies    service = UserService(database=mock_db)    user_data = {"email": "[email protected]", "name": "Test"}
    # Act: Perform the action being tested    result = service.create_user(user_data)
    # Assert: Verify the outcome    assert result.email == "[email protected]"    assert result.id is not None

4. Use Fixtures for Common Setup

python
# ❌ BAD: Repeated setupdef 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.fixturedef db():    database = setup_database()    yield database    database.close()
@pytest.fixturedef 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

python
# ❌ BAD: Duplicate test codedef 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

python
# ❌ BAD: Testing multiple thingsdef 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 testsdef 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

python
@pytest.mark.unitdef test_pure_function():    pass
@pytest.mark.integration@pytest.mark.slowdef test_database_integration():    pass
@pytest.mark.smokedef test_critical_path():    pass

8. Mock External Dependencies

python
# ✅ GOOD: Mock external APIdef 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 testdef 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

python
# WRONG: Tests should be independentclass 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:

python
@pytest.fixturedef 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

python
# WRONG: Database not cleaned updef test_user_creation():    db = setup_database()    user = create_user(db)    assert user.id is not None    # Database connection not closed!

Correct:

python
@pytest.fixturedef db():    database = setup_database()    yield database    database.close()  # Cleanup

❌ Anti-Pattern 3: Testing Implementation Details

python
# WRONG: Testing internal implementationdef test_user_service_uses_cache():    service = UserService()    service.get_user(1)    assert service._cache.has_key(1)  # Testing internal cache!

Correct:

python
# Test behavior, not implementationdef test_user_service_returns_user():    service = UserService()    user = service.get_user(1)    assert user.id == 1

❌ Anti-Pattern 4: Not Using pytest Features

python
# WRONG: Using unittest assertionsimport unittest
def test_addition():    result = add(2, 3)    unittest.TestCase().assertEqual(result, 5)

Correct:

python
# Use pytest's rich assertionsdef test_addition():    assert add(2, 3) == 5

❌ Anti-Pattern 5: Overly Complex Fixtures

python
# WRONG: Fixture does too much@pytest.fixturedef 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:

python
# Separate, composable fixtures@pytest.fixturedef db():    return setup_db()
@pytest.fixturedef user(db):    return create_user(db)
@pytest.fixturedef session(user):    return login(user)

Quick Reference

Common Commands

bash
# Run all testspytest
# Verbose outputpytest -v
# Show print statementspytest -s
# Run specific filepytest tests/test_api.py
# Run specific testpytest tests/test_api.py::test_create_user
# Run by markerpytest -m unitpytest -m "not slow"
# Run with coveragepytest --cov=app --cov-report=html
# Parallel executionpytest -n auto  # Requires pytest-xdist
# Stop on first failurepytest -x
# Show local variables on failurepytest -l
# Run last failed testspytest --lf
# Run failed tests firstpytest --ff

pytest.ini Template

ini
[pytest]# Minimum pytest versionminversion = 7.0
# Test discovery patternspython_files = test_*.py *_test.pypython_classes = Test*python_functions = test_*
# Test pathstestpaths = tests
# Command line optionsaddopts =    -v    --strict-markers    --cov=app    --cov-report=html    --cov-report=term-missing    --cov-fail-under=80
# Markersmarkers =    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 modeasyncio_mode = auto

conftest.py Template

python
# conftest.pyimport pytestfrom fastapi.testclient import TestClientfrom app.main import app
# FastAPI client fixture@pytest.fixturedef client():    return TestClient(app)
# Database fixture@pytest.fixture(scope="function")def db():    database = setup_test_database()    yield database    database.close()
# Mock user fixture@pytest.fixturedef mock_user():    return {"id": 1, "email": "[email protected]", "name": "Test User"}
# Custom pytest configurationdef 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:

  1. RED Phase: Write Failing Test

    python
    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
  2. GREEN Phase: Make It Pass

    python
    def authenticate(user):    # Minimum code to pass the test    if user.username == 'alice' and user.password == 'secret123':  # pragma: allowlist secret        return AuthResult(is_authenticated=True)    return AuthResult(is_authenticated=False)
  3. REFACTOR Phase: Improve Code

    python
    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)

python
def test_user_creation():    # Arrange: Set up test data    user_data = {'username': 'alice', 'email': '[email protected]'}
    # Act: Perform the action    user = create_user(user_data)
    # Assert: Verify outcome    assert user.username == 'alice'    assert user.email == '[email protected]'

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

python
# Use pytest's built-in debuggingpytest tests/test_auth.py -vv --pdb  # Drop into debugger on failurepytest tests/test_auth.py -x         # Stop on first failurepytest tests/test_auth.py -k "auth"  # Run only auth-related tests
# Add strategic print/loggingdef 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

bash
# Run full test suitepytest
# Run with coveragepytest --cov=src --cov-report=html
# Verify specific test patternspytest -k "auth or login" -v

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


Python Code-Quality Anti-Patterns

Clean code under test is easier to test, and several Python quality defects directly cause flaky or silently-passing tests (overly broad except, malformed exception classes, identity-vs-equality bugs). Code-quality anti-patterns now live in their own dedicated skill rather than this testing skill:

See the python-code-quality skill (toolchains/python/quality/code-quality) for the highest-value Python anti-patterns — exception-hierarchy correctness, singleton identity comparison, narrow exception handling, wildcard-import avoidance, magic-number naming, and dead-local removal — with non-compliant vs compliant examples and how to test each. For the project-wide severity-tagged review checklist, see the code-review-standards skill.


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

Source and attribution

Source:bobmatnyc/claude-mpm-skillsintoolchains/python/testing/pytestat commit718070a

License: MIT

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal