CtrlK
BlogDocsLog inGet started
Tessl Logo

pytest-testing-skill

Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.

84

Quality

81%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

SKILL.md
Quality
Evals
Security

Pytest Testing Skill

Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.

Prerequisites

  • Python >= 3.11
  • pip or Poetry

Scaffold Command

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

# Optional plugins
pip install pytest-xdist        # Parallel test execution
pip install pytest-randomly     # Randomize test order
pip install factory-boy         # Test data factories
pip install httpx               # Async HTTP client for testing FastAPI

Project Structure

app/
  __init__.py
  main.py
  services/
    user_service.py
  repositories/
    user_repository.py
tests/
  __init__.py
  conftest.py                     # Root-level fixtures and configuration
  unit/
    __init__.py
    conftest.py                   # Unit-test-specific fixtures
    test_user_service.py
  integration/
    __init__.py
    conftest.py                   # Integration-test-specific fixtures (DB, API client)
    test_user_api.py
  factories.py                    # Test data factories
pyproject.toml                    # pytest configuration

Key Conventions

  • Test files start with test_. Test functions start with test_.
  • Use conftest.py files for shared fixtures. Place them at the appropriate directory level (root for global, subdirectory for scoped).
  • Fixtures define reusable setup. Prefer fixtures over setUp/tearDown methods.
  • Use @pytest.mark.parametrize for data-driven tests instead of duplicating test functions.
  • Use @pytest.mark.asyncio for async test functions.
  • Keep unit tests fast and isolated (mock external dependencies). Integration tests hit real databases/APIs.
  • Use markers to categorize tests (e.g., @pytest.mark.slow, @pytest.mark.integration).

Essential Patterns

Pytest Configuration (pyproject.toml)

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
]
addopts = [
    "--strict-markers",
    "--strict-config",
    "-ra",
    "--tb=short",
]
filterwarnings = [
    "error",
    "ignore::DeprecationWarning",
]

Root conftest.py (tests/conftest.py)

import pytest
from unittest.mock import AsyncMock


@pytest.fixture
def mock_user():
    """A sample user dict for testing."""
    return {
        "id": 1,
        "email": "test@example.com",
        "name": "Test User",
        "role": "user",
    }


@pytest.fixture
def mock_users(mock_user):
    """A list of sample users."""
    return [
        mock_user,
        {"id": 2, "email": "admin@example.com", "name": "Admin User", "role": "admin"},
    ]

Unit Test — Basic (tests/unit/test_user_service.py)

import pytest
from unittest.mock import AsyncMock, patch
from app.services.user_service import UserService


class TestUserService:
    @pytest.fixture(autouse=True)
    def setup(self):
        self.mock_repo = AsyncMock()
        self.service = UserService(repository=self.mock_repo)

    async def test_get_user_returns_user_when_found(self, mock_user):
        self.mock_repo.find_by_id.return_value = mock_user

        result = await self.service.get_user(1)

        assert result == mock_user
        self.mock_repo.find_by_id.assert_called_once_with(1)

    async def test_get_user_raises_when_not_found(self):
        self.mock_repo.find_by_id.return_value = None

        with pytest.raises(ValueError, match="User not found"):
            await self.service.get_user(999)

    async def test_create_user_hashes_password(self):
        self.mock_repo.create.return_value = {"id": 1, "email": "new@example.com"}

        await self.service.create_user(
            email="new@example.com",
            name="New User",
            password="plaintext123",
        )

        call_args = self.mock_repo.create.call_args[1]
        assert call_args["password"] != "plaintext123"

Parametrize — Data-Driven Tests

import pytest
from app.utils.validators import validate_email


@pytest.mark.parametrize(
    "email, expected",
    [
        ("user@example.com", True),
        ("user.name@domain.co.uk", True),
        ("invalid", False),
        ("@no-local.com", False),
        ("no-at-sign.com", False),
        ("", False),
    ],
    ids=[
        "valid-basic",
        "valid-subdomain",
        "no-at-sign-no-domain",
        "no-local-part",
        "missing-at",
        "empty-string",
    ],
)
def test_validate_email(email: str, expected: bool):
    assert validate_email(email) is expected

Fixture Scopes and Yield

import pytest
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker


@pytest.fixture(scope="session")
def engine():
    """Create engine once for the entire test session."""
    engine = create_async_engine("postgresql+asyncpg://test:test@localhost:5432/testdb")
    yield engine
    # Cleanup runs after all tests complete


@pytest.fixture(scope="function")
async def session(engine):
    """Create a new session per test, rolled back after each test."""
    async_session = async_sessionmaker(engine, class_=AsyncSession)
    async with async_session() as session:
        async with session.begin():
            yield session
            await session.rollback()

pytest-mock — Mocker Fixture

def test_send_notification_calls_email_service(mocker):
    mock_send = mocker.patch("app.services.email_service.send_email", return_value=True)
    mocker.patch("app.services.email_service.get_template", return_value="<html>Hello</html>")

    from app.services.notification_service import notify_user

    notify_user("user@example.com", "Welcome!")

    mock_send.assert_called_once_with(
        to="user@example.com",
        subject="Welcome!",
        body="<html>Hello</html>",
    )

Async Tests with pytest-asyncio

import pytest
import httpx
from app.main import app  # FastAPI app


@pytest.fixture
async def client():
    """Async HTTP client for FastAPI integration tests."""
    async with httpx.AsyncClient(app=app, base_url="http://test") as client:
        yield client


class TestUserAPI:
    async def test_create_user(self, client: httpx.AsyncClient):
        response = await client.post(
            "/api/users",
            json={"email": "new@example.com", "name": "New User", "password": "secret123"},
        )

        assert response.status_code == 201
        data = response.json()
        assert data["email"] == "new@example.com"
        assert "password" not in data

    async def test_get_user_not_found(self, client: httpx.AsyncClient):
        response = await client.get("/api/users/99999")

        assert response.status_code == 404
        assert response.json()["detail"] == "User not found"

Factory Boy Integration (tests/factories.py)

import factory
from app.models.user import User


class UserFactory(factory.Factory):
    class Meta:
        model = User

    id = factory.Sequence(lambda n: n + 1)
    email = factory.LazyAttribute(lambda obj: f"user{obj.id}@example.com")
    name = factory.Faker("name")
    role = "user"


# Usage in tests:
# user = UserFactory()
# admin = UserFactory(role="admin", email="admin@test.com")
# users = UserFactory.build_batch(5)

Custom Markers

import pytest


@pytest.mark.slow
def test_complex_computation():
    """This test takes a long time."""
    result = expensive_computation()
    assert result > 0


@pytest.mark.integration
async def test_database_query(session):
    """This test requires a real database."""
    users = await session.execute(select(User))
    assert users.scalars().all() is not None

conftest.py for Integration Tests (tests/integration/conftest.py)

import pytest
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.database import Base
from app.models import *  # noqa: ensure models are loaded


@pytest.fixture(scope="session")
async def engine():
    engine = create_async_engine("postgresql+asyncpg://test:test@localhost:5432/testdb")
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield engine
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await engine.dispose()


@pytest.fixture
async def session(engine):
    session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    async with session_factory() as session:
        yield session
        await session.rollback()

Mocking External HTTP APIs (responses library)

import responses

@responses.activate
def test_external_api_call():
    responses.add(
        responses.GET,
        "https://api.example.com/users",
        json=[{"id": 1, "name": "Alice"}],
        status=200,
    )
    result = fetch_users()  # Your function that calls the external API
    assert len(result) == 1
    assert result[0]["name"] == "Alice"


@responses.activate
def test_external_api_error():
    responses.add(
        responses.GET,
        "https://api.example.com/users",
        json={"error": "Service unavailable"},
        status=503,
    )
    with pytest.raises(ExternalServiceError):
        fetch_users()
# Install the responses library
pip install responses

Common Commands

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run a specific file
pytest tests/unit/test_user_service.py

# Run a specific test function
pytest tests/unit/test_user_service.py::TestUserService::test_get_user_returns_user_when_found

# Run tests matching a keyword
pytest -k "test_create"

# Run tests by marker
pytest -m "not slow"
pytest -m integration

# Run with coverage
pytest --cov=app --cov-report=term-missing --cov-report=html

# Run in parallel (pytest-xdist)
pytest -n auto

# Run with detailed failure output
pytest --tb=long

# Stop on first failure
pytest -x

# Re-run only failed tests
pytest --lf

# Show slowest 10 tests
pytest --durations=10

# Generate JUnit XML report (for CI)
pytest --junitxml=report.xml

Integration Notes

  • FastAPI: Use httpx.AsyncClient(app=app) for async integration tests. Override dependencies with app.dependency_overrides for mocking database sessions.
  • SQLAlchemy: Pair with sqlalchemy-starter skill. Use session-scoped engine fixture and function-scoped session with rollback.
  • CI/CD: Pair with github-actions-ci skill. Run pytest --cov --junitxml=report.xml in CI. Upload coverage artifacts.
  • Docker: Use docker compose to spin up test databases. Set DATABASE_URL as an environment variable in CI.
  • Type Checking: Pair with mypy. Run mypy separately from pytest (they serve different purposes).

Test Database Setup

# docker-compose.test.yml
services:
  test-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data  # RAM-backed for speed

Use DATABASE_URL=postgresql+asyncpg://test:test@localhost:5433/testdb in your test environment. Start with docker compose -f docker-compose.test.yml up -d before running integration tests.

Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.