La Anatomía de una Suite pytest
Una prueba de pytest parece engañosamente simple: un nombre de función que comienza con test_, una declaración assert, listo; pero casi todas las decisiones no obvias de pytest se remontan a la mecánica que se esconde debajo de esa simplicidad: cómo se "encuentran" e inyectan las fixtures, por qué la recolección puede fallar antes de que se ejecute una sola prueba, y por qué el alcance de la fixture que eliges es realmente una compensación entre el aislamiento y el coste de configuración.
Configuración de pytest pone en marcha un proyecto con fixtures y configuración reales; las páginas específicas de la herramienta que siguen - Fixtures, Parametrización, Mocking y Patching, Pruebas Basadas en Propiedades - cada una profundiza en un mecanismo. Esta página se mantiene un nivel por encima: qué es realmente una prueba bajo el modelo de pytest, cómo las fixtures forman un grafo de dependencias y cómo las piezas se relacionan entre sí independientemente del mecanismo específico que una prueba dada utilice.
Resumen
- pytest resuelve las dependencias de una prueba a través de la inyección por nombre de parámetro, construyendo un grafo de fixtures detrás de escena en lugar de requerir conexiones explícitas de configuración/desmontaje.
- Por Qué Importa: Comprender la resolución y el alcance de las fixtures explica casi todos los fallos confusos de pytest: una fixture "no encontrada", estado compartido inesperado entre pruebas, o una suite que pasa localmente pero falla en un orden de recolección diferente.
- Conceptos Clave: recolección, fixture, alcance (scope), inyección de dependencias, parametrización, doble de prueba (test double).
- Cuándo Usar Este Modelo: Diagnosticar una fixture que no se encuentra o se comparte inesperadamente, decidir entre un mock y una dependencia real, elegir el alcance de la fixture para un recurso costoso, y explicar por qué un error de importación rompe toda la suite en lugar de una sola prueba.
- Limitaciones / Compensaciones: La resolución implícita de fixtures basada en nombres de pytest es potente pero no obvia: un grafo de fixtures grande a través de muchos archivos
conftest.pypuede ser genuinamente difícil de rastrear sin herramientas (pytest --fixtures), y ninguna cantidad de pruebas exitosas prueba que los caminos no probados son correctos. - Temas Relacionados: la pirámide de pruebas (unitarias vs. integración), dobles de prueba y límites de mocking, puertas de integración continua, pruebas basadas en propiedades.
Fundamentos
Antes de que pytest ejecute nada, realiza la recolección (collection): recorre los directorios de pruebas, importa cada archivo que coincide con su patrón de descubrimiento (test_*.py o *_test.py), y recopila cada función cuyo nombre comienza con test_ en una lista de elementos de prueba. Es por eso que un error de sintaxis o una mala importación en un archivo de prueba rompe todo el paso de recolección en lugar de fallar una prueba: el archivo tiene que importarse correctamente antes de que pytest pueda siquiera ver lo que hay dentro.
Una fixture es una función decorada con @pytest.fixture que proporciona la dependencia de una prueba: una conexión a la base de datos, datos de ejemplo, un cliente de API; y pytest la inyecta puramente haciendo coincidir nombres de parámetros: una función de prueba que declara def test_x(sample_user): recibe la fixture sample_user que esté visible en el alcance, sin necesidad de importación o conexión explícita. Esa resolución implícita basada en nombres es la mecánica distintiva de pytest, y también es la fuente de la mayoría de la confusión de "por qué mi fixture no se está utilizando", ya que un error tipográfico en el nombre del parámetro produce silenciosamente un error de "fixture no encontrada" en lugar de un no-operativo silencioso.
Una analogía útil: piensa en las fixtures como un contenedor de inyección de dependencias que resuelve por nombre en lugar de por tipo: solicita db_session como parámetro, y pytest recorre los archivos conftest.py (el directorio actual, luego los directorios padre) para encontrar la fixture más cercana con ese nombre exacto, de la misma manera que un framework de DI en otro idioma resuelve un argumento de constructor a un proveedor registrado.
import pytest
@pytest.fixture
def sample_user():
return {"id": 1, "email": "ada@example.com"}
def test_user_email(sample_user): # inyectada por nombre de parámetro
assert "@" in sample_user["email"]Mecánicas e Interacciones
El alcance de la fixture (fixture scope) determina cuánto tiempo vive el valor de una fixture antes de que pytest lo desmonte y cree uno nuevo, y elegir un alcance es realmente elegir un punto en una compensación entre el aislamiento y el coste de configuración. Una fixture con alcance function (el predeterminado) se recrea para cada prueba individual, maximizando el aislamiento a costa de repetir el trabajo de configuración; una fixture con alcance session se crea una vez para toda la ejecución de pruebas, minimizando el coste pero requiriendo que el desarrollador garantice que nada muta el estado compartido entre pruebas que dependen de ella. Un motor de base de datos es el recurso de libro de texto con alcance session (costoso de crear, seguro de compartir porque no contiene estado por prueba en sí mismo), mientras que la transacción que envuelve cada prueba típicamente se mantiene con alcance function específicamente para que las escrituras de una prueba nunca se filtren a la siguiente.
Las fixtures se componen simplemente solicitándose entre sí como parámetros, lo que significa que el lenguaje de "grafo de dependencias" no es solo una metáfora: una fixture que depende de otra fixture es un borde real en un grafo real que pytest resuelve antes de ejecutar la prueba, y pytest impone una restricción dura en ese grafo: una fixture de alcance más amplio no puede depender de una de alcance más estrecho, porque una fixture de alcance session creada una vez no tiene forma de recibir un valor fresco de alcance function en cada prueba.
Los dobles de prueba (test doubles) - sustitutos de dependencias reales - interactúan con este mismo grafo de fixtures en lugar de reemplazarlo: una fixture monkeypatch o una llamada unittest.mock.patch sustituyen un nodo en el grafo de dependencias (un cliente de red, un reloj, una variable de entorno) para que una prueba pueda aislar la lógica que realmente le importa. Elegir un doble frente a una dependencia real es una versión más pequeña de la misma compensación entre aislamiento y realismo que toman las decisiones de alcance: un cliente HTTP mockeado se ejecuta en microsegundos pero no puede detectar un error de serialización real, mientras que un cliente real contra un servidor de prueba detecta ese error a costa de una configuración más lenta y compleja.
@pytest.fixture(scope="session")
def engine():
eng = create_engine("sqlite:///:memory:")
yield eng # el desmontaje se ejecuta después de que finaliza la sesión
eng.dispose()
@pytest.fixture
def db_session(engine): # depende de la fixture con alcance de sesión
conn = engine.connect()
txn = conn.begin()
yield Session(bind=conn)
txn.rollback() # alcance de función: aísla cada pruebaConsideraciones y Aplicaciones Avanzadas
La parametrización (@pytest.mark.parametrize) y las pruebas basadas en propiedades (Hypothesis) se encuentran en el mismo espectro subyacente en lugar de ser características no relacionadas: ambas ejecutan un cuerpo de prueba con muchos entradas, pero la parametrización enumera una lista fija de casos elegida por el desarrollador, mientras que Hypothesis genera cientos de entradas a partir de una estrategia declarada y reduce (shrinks) cualquier fallo al ejemplo más pequeño que lo reproduce. La parametrización es la herramienta adecuada cuando los casos límite ya se conocen (un valor límite, una cadena conocida como problemática); Hypothesis justifica su coste cuando el dominio de entrada es lo suficientemente grande como para que un humano que enumera casos manualmente se pierda predeciblemente el que realmente rompe la función.
La arquitectura de plugins de pytest es lo que convierte un núcleo bastante pequeño (recolección, fixtures, reescritura de aserciones) en un ecosistema que cubre código asíncrono, cobertura y pruebas de API web sin hinchar la herramienta base: pytest-asyncio enseña al ejecutor a esperar funciones de prueba corutina, pytest-cov envuelve coverage.py para informar qué líneas se ejecutaron, y cada plugin se engancha a la misma maquinaria de recolección y fixtures en lugar de inventar una paralela. Esa arquitectura también es la razón por la que mezclar versiones de plugins descuidadamente puede producir fallos confusos: un plugin que se engancha a la configuración de fixtures puede cambiar el comportamiento de fixtures que ni siquiera toca directamente.
La cobertura (coverage) merece el mismo escrutinio aquí que en cualquier discusión sobre pruebas: informa el porcentaje de líneas o ramas que pytest ejecutó durante la ejecución, lo cual es una afirmación completamente diferente a lo que se verificó significativamente. Una prueba que llama a una función y no afirma nada sobre su resultado todavía cuenta como cobertura de cada línea que esa función ejecutó sin probar nada; la cobertura es útil para encontrar código que nadie ejercita en absoluto, y un objetivo pobre a perseguir por sí mismo.
| Enfoque | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Fixtures (alcance de función) | Aislamiento máximo, sin estado filtrado | Coste de configuración pagado en cada prueba | Opción predeterminada para la mayoría de las dependencias de prueba |
| Fixtures (alcance de sesión/módulo) | Amortiza la configuración costosa una vez | Requiere cuidado de que nada mute el estado compartido | Motores de BD, instancias de aplicaciones, cualquier cosa costosa y sin estado para compartir |
parametrize | Explícito, legible, rápido para casos conocidos | Omite casos límite que nadie pensó en enumerar | Valores límite, entradas conocidas como problemáticas, matrices pequeñas |
| Hypothesis (basado en propiedades) | Encuentra casos límite que nadie enumeró, reduce fallos automáticamente | Más lento, requiere enmarcar invariantes en lugar de ejemplos | Invariantes de analizadores, serializadores, matemáticas/ida y vuelta |
Conceptos Erróneos Comunes
- "Las fixtures son solo ayudantes de configuración/desmontaje, como
setUp/tearDown." Se resuelven a través de un grafo de dependencias basado en nombres que pytest construye e inyecta automáticamente, más cercano a la inyección de dependencias que al código imperativo de configuración. - "Una fixture con alcance
sessiones siempre más rápida y, por lo tanto, siempre mejor." Amortiza el coste en toda la ejecución, pero solo de forma segura cuando nada de ella muta entre pruebas; mal aplicada a algo con estado por prueba, rompe silenciosamente el aislamiento en lugar de ahorrar tiempo. - "Más casos parametrizados siempre significa mejor cobertura de casos límite." Los casos enumerados solo cubren lo que un humano pensó en escribir; un gran dominio de entrada a menudo tiene un caso límite que la parametrización nunca consideró, que es exactamente la brecha que Hypothesis está diseñado para cerrar.
- "100% de cobertura significa que la suite es exhaustiva." La cobertura cuenta las líneas ejecutadas, no las aserciones que verificaron significativamente el comportamiento; una prueba puede ejecutar cada línea y no verificar nada.
- "Un error de importación en un archivo de prueba solo falla las pruebas de ese archivo." La recolección tiene que importar correctamente un archivo antes de que pytest pueda ver alguna prueba dentro de él, por lo que una importación rota puede impedir que pytest descubra pruebas que de otro modo pasarían.
Preguntas Frecuentes
¿Cómo decide pytest realmente qué funciones son pruebas?
Durante la recolección, importa cada archivo que coincide con su patrón de descubrimiento (típicamente test_*.py) y recopila cada función cuyo nombre comienza con test_; esto ocurre antes de que se ejecute cualquier cuerpo de prueba.
¿Cómo se "inyecta" una fixture en una función de prueba?
Haciendo coincidir el nombre del parámetro de la función de prueba con una fixture del mismo nombre visible en el alcance; pytest recorre los archivos conftest.py para encontrar la coincidencia más cercana, sin necesidad de una importación explícita.
¿Cuál es la diferencia práctica entre los alcances de las fixtures?
El alcance controla cuánto tiempo vive el valor de una fixture antes de ser recreado: el alcance function lo recrea por prueba para un aislamiento máximo, mientras que el alcance session lo crea una vez para toda la ejecución para amortizar la configuración costosa, a costa de necesitar garantizar que siga siendo seguro compartirlo.
¿Por qué una fixture con alcance de sesión no puede depender de una con alcance de función?
Porque la fixture de sesión se crea una vez y no tendría forma de recibir un valor fresco de una fixture destinada a ser recreada para cada prueba individual; pytest impone que un alcance más amplio no pueda depender de uno más estrecho.
¿Cuál es la diferencia entre `yield` y `return` en una fixture?
yield permite que el código posterior se ejecute como desmontaje una vez que la prueba (o el alcance) finaliza; return proporciona un valor sin fase de desmontaje.
¿Cuándo debería usar `parametrize` en lugar de Hypothesis?
Usa parametrize cuando los casos límite ya se conocen y son pocos en número; recurre a Hypothesis cuando el dominio de entrada es lo suficientemente grande como para que un humano que enumera casos manualmente probablemente se pierda el que realmente rompe la función.
¿Qué hace realmente el "shrinking" de Hypothesis?
Cuando una entrada generada causa un fallo, Hypothesis busca la entrada más pequeña que aún lo reproduce, convirtiendo un ejemplo de fallo grande y confuso en uno mínimo y legible.
¿Es siempre más seguro mockear una dependencia que usar la real?
No necesariamente: un mock aísla una prueba del coste y la inestabilidad de una dependencia real, pero tampoco puede detectar un error que solo el comportamiento de la dependencia real expondría, como un error de sintaxis SQL real.
¿Por qué un error de importación en un archivo de prueba causa más fallos de lo esperado?
La recolección tiene que importar correctamente un archivo antes de que pytest pueda identificar cualquier prueba dentro de él, por lo que una importación rota puede impedir el descubrimiento de todas las pruebas en ese archivo, no solo la que está cerca de la línea rota.
¿Qué proporciona realmente la arquitectura de plugins de pytest?
Un conjunto compartido de hooks para la recolección, fixtures y reportes que plugins como pytest-asyncio o pytest-cov extienden, en lugar de que cada plugin implemente su propia maquinaria de ejecución de pruebas paralela.
¿Debería apuntar a un porcentaje de cobertura específico?
No directamente como objetivo: la cobertura te dice qué código se ejecutó, no qué se afirmó significativamente, por lo que es más útil para encontrar código completamente sin probar que para juzgar qué tan bien probado está el código probado.
¿Por qué pytest recomienda `conftest.py` en lugar de importar fixtures directamente?
Los archivos conftest.py se descubren automáticamente en cada nivel de directorio, por lo que las fixtures definidas allí se vuelven disponibles para todas las pruebas en ese directorio y los inferiores sin una importación explícita, que es lo que hace que el grafo de resolución basado en nombres funcione en todo un árbol de pruebas.
Relacionados
- Configuración de pytest - diseño del proyecto, descubrimiento y configuración en la práctica
- Fixtures - alcances, fábricas y composición del grafo de dependencias
- Parametrización - pruebas basadas en datos enumerados
- Mocking y Patching - sustitución de nodos en el grafo de fixtures
- Pruebas Basadas en Propiedades - entradas generadas y reducción
- Cobertura y Reportes - qué mide la cobertura y sus límites
Versiones de la Pila: Esta página fue escrita para Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.