Communitygithub.com

python-testing

Estrategias de prueba en Python usando pytest, metodología TDD, fixtures, mocks, parametrización y requisitos de cobertura. Úsalo al escribir pruebas con pytest: fixtures, mocks, parametrización o cobertura.

¿Qué es python-testing?

Este skill proporciona un conjunto completo de patrones y estrategias para pruebas en Python, centrándose en el framework pytest, la metodología TDD (Red-Green-Refactor), fixtures, mocks, parametrización y requisitos de cobertura. Está diseñado para desarrolladores que necesitan escribir pruebas robustas y mantenibles para aplicaciones Python, abarcando desde pruebas unitarias hasta integración y pruebas asíncronas. El skill detalla cómo estructurar fixtures con diferentes ámbitos (función, módulo, sesión), cómo parametrizar pruebas para cubrir múltiples casos de entrada, y cómo utilizar mocking con `unittest.mock` para aislar dependencias externas. Además, incluye guías sobre organización de directorios de pruebas, configuración de marcadores personalizados, y uso de herramientas como `pytest-cov` para medir la cobertura. Es compatible con cualquier proyecto Python que use pytest, y su contenido está basado en las mejores prácticas de la comunidad, incluyendo ejemplos de código listos para usar.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/python-testing

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Patrones de Pruebas en Python

Estrategias integrales de prueba para aplicaciones Python usando pytest, metodología TDD y mejores prácticas.

Cuándo Activar

  • Al escribir nuevo código Python (seguir TDD: rojo, verde, refactorizar)
  • Al diseñar suites de pruebas para proyectos Python
  • Al revisar la cobertura de pruebas Python
  • Al configurar infraestructura de pruebas

Filosofía Central de Pruebas

Desarrollo Guiado por Pruebas (TDD)

Sigue siempre el ciclo TDD:

  1. ROJO: Escribe una prueba fallida para el comportamiento deseado
  2. VERDE: Escribe el código mínimo para que la prueba pase
  3. REFACTORIZAR: Mejora el código mientras mantienes las pruebas en verde
# Paso 1: Escribir prueba fallida (ROJO)
def test_sumar_numeros():
    resultado = suma(2, 3)
    assert resultado == 5

# Paso 2: Escribir implementación mínima (VERDE)
def suma(a, b):
    return a + b

# Paso 3: Refactorizar si es necesario (REFACTORIZAR)

Requisitos de Cobertura

  • Objetivo: 80%+ de cobertura de código
  • Rutas críticas: Se requiere 100% de cobertura
  • Usa pytest --cov para medir la cobertura
pytest --cov=mi_paquete --cov-report=term-missing --cov-report=html

Fundamentos de pytest

Estructura Básica de Prueba

import pytest

def test_suma():
    """Prueba suma básica."""
    assert 2 + 2 == 4

def test_mayusculas_cadena():
    """Prueba conversión a mayúsculas."""
    texto = "hola"
    assert texto.upper() == "HOLA"

def test_agregar_lista():
    """Prueba agregar a lista."""
    elementos = [1, 2, 3]
    elementos.append(4)
    assert 4 in elementos
    assert len(elementos) == 4

Aserciones

# Igualdad
assert resultado == esperado

# Desigualdad
assert resultado != inesperado

# Veracidad
assert resultado  # Verdadero
assert not resultado  # Falso
assert resultado is True  # Exactamente True
assert resultado is False  # Exactamente False
assert result is None  # Exactamente None

# Pertenencia
assert elemento in colección
assert elemento not in colección

# Comparaciones
assert resultado > 0
assert 0 <= resultado <= 100

# Verificación de tipo
assert isinstance(resultado, str)

# Prueba de excepciones (enfoque preferido)
with pytest.raises(ValueError):
    raise ValueError("mensaje de error")

# Verificar mensaje de excepción
with pytest.raises(ValueError, match="entrada no válida"):
    raise ValueError("entrada no válida proporcionada")

# Verificar atributos de excepción
with pytest.raises(ValueError) as exc_info:
    raise ValueError("mensaje de error")
assert str(exc_info.value) == "mensaje de error"

Fixtures

Uso Básico de Fixtures

import pytest

@pytest.fixture
def datos_muestra():
    """Fixture que proporciona datos de muestra."""
    return {"nombre": "Alicia", "edad": 30}

def test_datos_muestra(datos_muestra):
    """Prueba usando el fixture."""
    assert datos_muestra["nombre"] == "Alicia"
    assert datos_muestra["edad"] == 30

Fixture con Configuración/Limpieza

@pytest.fixture
def base_datos():
    """Fixture con configuración y limpieza."""
    # Configuración
    db = BaseDatos(":memoria:")
    db.crear_tablas()
    db.insertar_datos_prueba()

    yield db  # Proporcionar a la prueba

    # Limpieza
    db.cerrar()

def test_consulta_base_datos(base_datos):
    """Prueba operaciones de base de datos."""
    resultado = base_datos.consulta("SELECT * FROM usuarios")
    assert len(resultado) > 0

Ámbitos de Fixture

# Ámbito de función (predeterminado) - se ejecuta para cada prueba
@pytest.fixture
def archivo_temporal():
    with open("temp.txt", "w") as f:
        yield f
    os.remove("temp.txt")

# Ámbito de módulo - se ejecuta una vez por módulo
@pytest.fixture(scope="module")
def db_modulo():
    db = BaseDatos(":memoria:")
    db.crear_tablas()
    yield db
    db.cerrar()

# Ámbito de sesión - se ejecuta una vez por sesión de prueba
@pytest.fixture(scope="session")
def recurso_compartido():
    recurso = RecursoCostoso()
    yield recurso
    recurso.limpiar()

Fixture con Parámetros

@pytest.fixture(params=[1, 2, 3])
def numero(request):
    """Fixture parametrizado."""
    return request.param

def test_numeros(numero):
    """La prueba se ejecuta 3 veces, una por cada parámetro."""
    assert numero > 0

Usando Múltiples Fixtures

@pytest.fixture
def usuario():
    return Usuario(id=1, nombre="Alicia")

@pytest.fixture
def administrador():
    return Usuario(id=2, nombre="Admin", rol="admin")

def test_interaccion_usuario_admin(usuario, administrador):
    """Prueba usando múltiples fixtures."""
    assert administrador.puede_gestionar(usuario)

Fixtures Autouse

@pytest.fixture(autouse=True)
def resetear_config():
    """Se ejecuta automáticamente antes de cada prueba."""
    Config.reiniciar()
    yield
    Config.limpiar()

def test_sin_llamada_fixture():
    # resetear_config se ejecuta automáticamente
    assert Config.obtener_config("debug") is False

Conftest.py para Fixtures Compartidos

# tests/conftest.py
import pytest

@pytest.fixture
def cliente():
    """Fixture compartido para todas las pruebas."""
    app = crear_app(testing=True)
    with app.test_client() as client:
        yield client

@pytest.fixture
def cabeceras_auth(cliente):
    """Genera cabeceras de autenticación para pruebas de API."""
    respuesta = cliente.post("/api/login", json={
        "usuario": "test",
        "contraseña": "test"
    })
    token = respuesta.json["token"]
    return {"Authorization": f"Bearer {token}"}

Parametrización

Parametrización Básica

@pytest.mark.parametrize("entrada,esperado", [
    ("hola", "HOLA"),
    ("mundo", "MUNDO"),
    ("PyThOn", "PYTHON"),
])
def test_mayusculas(entrada, esperado):
    """La prueba se ejecuta 3 veces con diferentes entradas."""
    assert entrada.upper() == esperado

Múltiples Parámetros

@pytest.mark.parametrize("a,b,esperado", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_sumar(a, b, esperado):
    """Prueba suma con múltiples entradas."""
    assert sumar(a, b) == esperado

Parametrizar con IDs

@pytest.mark.parametrize("entrada,esperado", [
    ("[email protected]", True),
    ("invalido", False),
    ("@sin-dominio.com", False),
], ids=["email-valido", "falta-arroba", "falta-dominio"])
def test_validacion_email(entrada, esperado):
    """Prueba validación de email con IDs de prueba legibles."""
    assert es_email_valido(entrada) is esperado

Fixtures Parametrizados

@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def db(request):
    """Prueba contra múltiples motores de base de datos."""
    if request.param == "sqlite":
        return BaseDatos(":memoria:")
    elif request.param == "postgresql":
        return BaseDatos("postgresql://localhost/test")
    elif request.param == "mysql":
        return BaseDatos("mysql://localhost/test")

def test_operaciones_base_datos(db):
    """La prueba se ejecuta 3 veces, una por cada base de datos."""
    resultado = db.consulta("SELECT 1")
    assert resultado is not None

Marcadores y Selección de Pruebas

Marcadores Personalizados

# Marcar pruebas lentas
@pytest.mark.lento
def test_operacion_lenta():
    time.sleep(5)

# Marcar pruebas de integración
@pytest.mark.integracion
def test_integracion_api():
    respuesta = requests.get("https://api.example.com")
    assert respuesta.status_code == 200

# Marcar pruebas unitarias
@pytest.mark.unitaria
def test_logica_unitaria():
    assert calcular(2, 3) == 5

Ejecutar Pruebas Específicas

# Ejecutar solo pruebas rápidas
pytest -m "not lento"

# Ejecutar solo pruebas de integración
pytest -m integracion

# Ejecutar pruebas de integración o lentas
pytest -m "integracion or lento"

# Ejecutar pruebas marcadas como unitarias pero no lentas
pytest -m "unitaria and not lento"

Configurar Marcadores en pytest.ini

[pytest]
markers =
    lento: marca pruebas como lentas
    integracion: marca pruebas como pruebas de integración
    unitaria: marca pruebas como pruebas unitarias
    django: marca pruebas que requieren Django

Mocking y Patching

Mocking de Funciones

from unittest.mock import patch, Mock

@patch("mipaquete.llamada_api_externa")
def test_con_mock(llamada_api_mock):
    """Prueba con API externa simulada."""
    llamada_api_mock.return_value = {"estado": "éxito"}

    resultado = mi_funcion()

    llamada_api_mock.assert_called_once()
    assert resultado["estado"] == "éxito"

Mocking de Valores de Retorno

@patch("mipaquete.BaseDatos.conectar")
def test_conexion_base_datos(conectar_mock):
    """Prueba con conexión de base de datos simulada."""
    conectar_mock.return_value = MockConexion()

    db = BaseDatos()
    db.conectar()

    conectar_mock.assert_called_once_with("localhost")

Mocking de Excepciones

@patch("mipaquete.llamada_api")
def test_manejo_error_api(llamada_api_mock):
    """Prueba manejo de errores con excepción simulada."""
    llamada_api_mock.side_effect = ConnectionError("Error de red")

    with pytest.raises(ConnectionError):
        llamada_api()

    llamada_api_mock.assert_called_once()

Mocking de Administradores de Contexto

@patch("builtins.open", new_callable=mock_open)
def test_lectura_archivo(archivo_mock):
    """Prueba lectura de archivo con open simulado."""
    archivo_mock.return_value.read.return_value = "contenido del archivo"

    resultado = leer_archivo("test.txt")

    archivo_mock.assert_called_once_with("test.txt", "r")
    assert resultado == "contenido del archivo"

Usando Autospec

@patch("mipaquete.ConexionBD", autospec=True)
def test_autospec(db_mock):
    """Prueba con autospec para detectar mal uso de la API."""
    db = db_mock.return_value
    db.consulta("SELECT * FROM usuarios")

    # Esto fallaría si ConexionBD no tiene el método consulta
    db_mock.assert_called_once()

Mock de Instancias de Clase

class TestServicioUsuario:
    @patch("mipaquete.RepositorioUsuario")
    def test_crear_usuario(self, repo_mock):
        """Prueba creación de usuario con repositorio simulado."""
        repo_mock.return_value.guardar.return_value = Usuario(id=1, nombre="Alicia")

        servicio = ServicioUsuario(repo_mock.return_value)
        usuario = servicio.crear_usuario(nombre="Alicia")

        assert usuario.nombre == "Alicia"
        repo_mock.return_value.guardar.assert_called_once()

Mock de Propiedad

@pytest.fixture
def mock_config():
    """Crea un mock con una propiedad."""
    config = Mock()
    type(config).debug = PropertyMock(return_value=True)
    type(config).api_key = PropertyMock(return_value="clave-prueba")
    return config

def test_con_mock_config(mock_config):
    """Prueba con propiedades de configuración simuladas."""
    assert mock_config.debug is True
    assert mock_config.api_key == "clave-prueba"

Pruebas de Código Asíncrono

Pruebas Asíncronas con pytest-asyncio

import pytest

@pytest.mark.asyncio
async def test_funcion_asincrona():
    """Prueba función asíncrona."""
    resultado = await suma_asincrona(2, 3)
    assert resultado == 5

@pytest.mark.asyncio
async def test_asincrono_con_fixture(cliente_asincrono):
    """Prueba asíncrona con fixture asíncrono."""
    respuesta = await cliente_asincrono.get("/api/usuarios")
    assert respuesta.status_code == 200

Fixture Asíncrono

@pytest.fixture
async def cliente_asincrono():
    """Fixture asíncrono que proporciona un cliente de prueba asíncrono."""
    app = crear_app()
    async with app.test_client() as client:
        yield client

@pytest.mark.asyncio
async def test_endpoint_api(cliente_asincrono):
    """Prueba usando fixture asíncrono."""
    respuesta = await cliente_asincrono.get("/api/datos")
    assert respuesta.status_code == 200

Mocking de Funciones Asíncronas

@pytest.mark.asyncio
@patch("mipaquete.llamada_api_asincrona")
async def test_mock_asincrono(llamada_api_mock):
    """Prueba función asíncrona con mock."""
    llamada_api_mock.return_value = {"estado": "ok"}

    resultado = await mi_funcion_asincrona()

    llamada_api_mock.assert_awaited_once()
    assert resultado["estado"] == "ok"

Pruebas de Excepciones

Prueba de Excepciones Esperadas

def test_dividir_por_cero():
    """Prueba que dividir por cero lanza ZeroDivisionError."""
    with pytest.raises(ZeroDivisionError):
        dividir(10, 0)

def test_excepcion_personalizada():
    """Prueba excepción personalizada con mensaje."""
    with pytest.raises(ValueError, match="entrada no válida"):
        validar_entrada("inválido")

Prueba de Atributos de Excepción

def test_excepcion_con_detalles():
    """Prueba excepción con atributos personalizados."""
    with pytest.raises(ErrorPersonalizado) as exc_info:
        raise ErrorPersonalizado("error", codigo=400)

    assert exc_info.value.codigo == 400
    assert "error" in str(exc_info.value)

Pruebas de Efectos Secundarios

Prueba de Operaciones con Archivos

import tempfile
import os

def test_procesamiento_archivo():
    """Prueba procesamiento de archivo con archivo temporal."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
        f.write("contenido de prueba")
        ruta_temp = f.name

    try:
        resultado = procesar_archivo(ruta_temp)
        assert resultado == "procesado: contenido de prueba"
    finally:
        os.unlink(ruta_temp)

Prueba con el Fixture tmp_path de pytest

def test_con_ruta_temporal(tmp_path):
    """Prueba usando el fixture de ruta temporal incorporado de pytest."""
    archivo_prueba = tmp_path / "test.txt"
    archivo_prueba.write_text("hola mundo")

    resultado = procesar_archivo(str(archivo_prueba))
    assert resultado == "hola mundo"
    # tmp_path se limpia automáticamente

Prueba con el Fixture tmpdir

def test_con_directorio_temporal(tmpdir):
    """Prueba usando el fixture tmpdir de pytest."""
    archivo_prueba = tmpdir.join("test.txt")
    archivo_prueba.write("datos")

    resultado = procesar_archivo(str(archivo_prueba))
    assert resultado == "datos"

Organización de Pruebas

Estructura de Directorios

tests/
├── conftest.py                 # Fixtures compartidos
├── __init__.py
├── unit/                       # Pruebas unitarias
│   ├── __init__.py
│   ├── test_modelos.py
│   ├── test_utils.py
│   └── test_servicios.py
├── integracion/                # Pruebas de integración
│   ├── __init__.py
│   ├── test_api.py
│   └── test_base_datos.py
└── e2e/                        # Pruebas de extremo a extremo
    ├── __init__.py
    └── test_flujo_usuario.py

Clases de Prueba

class TestServicioUsuario:
    """Agrupar pruebas relacionadas en una clase."""

    @pytest.fixture(autouse=True)
    def setup(self):
        """Configuración se ejecuta antes de cada prueba en esta clase."""
        self.servicio = ServicioUsuario()

    def test_crear_usuario(self):
        """Prueba creación de usuario."""
        usuario = self.servicio.crear_usuario("Alicia")
        assert usuario.nombre == "Alicia"

    def test_eliminar_usuario(self):
        """Prueba eliminación de usuario."""
        usuario = self.servicio.crear_usuario("Bob")
        self.servicio.eliminar_usuario(usuario.id)
        assert not self.servicio.existe(usuario.id)

Fixtures Integradas de pytest

Uso de Fixtures Integradas

# tests/conftest.py
import pytest

@pytest.fixture
def datos_prueba():
    return {"a": 1, "b": 2}

# tests/test_ejemplo.py
def test_con_fixture_integrada(tmp_path, datos_prueba):
    """Usar fixtures integradas y personalizadas juntas."""
    archivo = tmp_path / "datos.json"
    # ...
    assert datos_prueba["a"] == 1

Fixtures Integradas Comunes

  • tmp_path: Directorio temporal único por función de prueba
  • tmpdir: Similar a tmp_path pero como objeto py.path.local
  • capsys: Captura salida estándar y de error
  • monkeypatch: Parchea objetos/diccionarios de forma segura
  • recwarn: Verifica advertencias emitidas

Mejores Prácticas

Nombrado de Pruebas

  • Prefijo test_ para funciones de prueba y archivos
  • Nombres descriptivos que expliquen la intención de la prueba
  • Seguir el patrón test_[unidad]_[comportamiento]_[resultado]

Estructura Arrange-Act-Assert (AAA)

def test_precio_total_con_impuesto():
    """Usar patrón AAA."""
    # Arrange (Preparar)
    carrito = Carrito()
    carrito.agregar(Producto("libro", 100.0))

    # Act (Actuar)
    total = carrito.calcular_total(impuesto=0.1)

    # Assert (Afirmar)
    assert total == 110.0

Pruebas Independientes y Aisladas

  • Cada prueba debe poder ejecutarse de forma independiente
  • No compartir estado mutable entre pruebas
  • Usar fixtures para configuración y limpieza

Consejos de Cobertura

  • Apuntar a un 80%+ de cobertura general
  • Cobertura del 100% para rutas críticas y manejo de errores
  • Usar # pragma: no cover con moderación para exclusiones justificadas

Referencia Rápida

Comandos Comunes de pytest

# Ejecutar todas las pruebas
pytest

# Ejecutar con salida detallada
pytest -v

# Ejecutar una prueba específica
pytest tests/test_modulo.py::test_funcion -v

# Detener en el primer fallo
pytest -x

# Ejecutar pruebas en paralelo
pytest -n auto

# Ejecutar pruebas que fallaron la última vez
pytest --lf

# Ejecutar pruebas que fallaron primero
pytest --ff

# Ejecutar pruebas por expresión
pytest -k "usuario and not lento"

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Skills relacionados