Los desarrolladores de Python eligen pytest porque se aparta del camino. Una prueba es simplemente una función cuyo nombre comienza con test_, una aserción es una simple sentencia assert, y el ejecutor hace el resto. Combínalo con la librería requests y tendrás un framework completo, con código primero, para automatizar pruebas de API, sin pesadas ceremonias.
Este tutorial muestra cómo construir un conjunto de pruebas de API con pytest. Configurarás el proyecto, escribirás tu primera prueba de solicitud, compartirás la lógica de configuración con *fixtures*, ejecutarás la misma prueba contra múltiples entradas con parametrize, y harás aserciones sobre el estado de la respuesta, el cuerpo y el esquema JSON. Cada ejemplo utiliza una API pública realista para que puedas adaptar el código directamente.
Configuración del proyecto
Instala las dos librerías que necesitas en un entorno virtual:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Una estructura limpia mantiene el conjunto de pruebas manejable a medida que crece:
api-tests/
conftest.py # shared fixtures (fixtures compartidas)
test_users.py # tests for the users endpoints (pruebas para los endpoints de usuarios)
test_orders.py # tests for the orders endpoints (pruebas para los endpoints de pedidos)
pytest.ini # configuration (configuración)
Pytest descubre las pruebas automáticamente. Los archivos deben comenzar con test_ o terminar con _test.py, las funciones deben comenzar con test_, y las clases de prueba deben comenzar con Test y no tener método __init__. Sigue esas reglas y nunca configurarás el descubrimiento manualmente. Si las pruebas automatizadas como disciplina son nuevas para ti, nuestro manual sobre qué son las pruebas automatizadas establece el contexto.
¿Por qué pytest para pruebas de API específicamente? La librería requests maneja el HTTP, y pytest maneja todo lo demás: descubrimiento, aserciones con salida de errores legible, configuración y limpieza a través de *fixtures*, ejecuciones basadas en datos a través de parametrize, y reportes. Ensamblas un framework completo de automatización de API a partir de dos librerías pequeñas y bien documentadas, en un lenguaje que tu equipo probablemente ya usa para la propia aplicación. Esa proximidad importa. Las pruebas que residen en el mismo repositorio que el código se mantienen honestas, porque un cambio que rompe algo y su prueba fallida aparecen en la misma solicitud de extracción.
Escribiendo tu primera prueba de API
Una prueba de API con pytest envía una solicitud y hace aserciones sobre la respuesta. Aquí hay una prueba contra un endpoint de usuarios:
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
Ejecuta el conjunto con pytest -v. Cada assert que falla produce un informe detallado que muestra el valor real, que es una de las mejores características de pytest. No necesitas métodos de aserción especiales; el framework reescribe las sentencias assert simples para dar una salida rica. Para el conjunto más amplio de comprobaciones que vale la pena hacer en una respuesta, consulta nuestra guía de aserción de API.
Compartiendo la configuración con *fixtures*
Repetir la URL base y una sesión HTTP en cada prueba es un desperdicio. Las *fixtures* resuelven esto. Una *fixture* es una función decorada con @pytest.fixture que produce un valor que las pruebas pueden solicitar nombrándolo como un parámetro.
Coloca las *fixtures* compartidas en conftest.py para que cada archivo de prueba pueda usarlas sin importar:
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
El argumento scope="session" significa que la sesión se crea una vez para toda la ejecución en lugar de por prueba. La palabra clave yield divide la configuración de la limpieza: el código antes de yield se ejecuta primero, el código después se ejecuta cuando la *fixture* sale de su alcance. Una prueba simplemente solicita lo que necesita:
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
Las *fixtures* son el reemplazo moderno de pytest para el estilo más antiguo setup_function y teardown_function. Se componen de forma limpia, admiten alcances y hacen explícitas las dependencias, por lo que la documentación oficial de *fixtures* de pytest las recomienda como el enfoque predeterminado.
Ejecutando una prueba contra múltiples entradas
Los endpoints de API suelen necesitar ser verificados contra muchas entradas: valores válidos, valores inválidos y casos límite. Escribir una función separada para cada uno es tedioso. El decorador @pytest.mark.parametrize ejecuta un cuerpo de prueba contra una lista de entradas:
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
Esto produce cuatro casos de prueba separados a partir de una función. Cada uno se ejecuta e informa de forma independiente, por lo que una única entrada incorrecta no oculta las demás. Parametrize es la respuesta incorporada de pytest a las pruebas basadas en datos. Cuando el conjunto de entradas crece mucho, cárgalo desde un archivo en su lugar; nuestra guía de pruebas de API basadas en datos con CSV y JSON cubre ese patrón. Si no estás seguro de qué código de estado debe devolver cada entrada, la referencia sobre qué códigos de estado HTTP deben usar las API REST es un compañero útil.
Aserción sobre el cuerpo de la respuesta y el esquema
Los códigos de estado son necesarios pero no suficientes. Una respuesta 200 con un cuerpo malformado sigue siendo un error. Haz aserciones directamente sobre el JSON analizado:
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
Para garantías más sólidas, valida el cuerpo contra un esquema JSON. Esto detecta desviaciones estructurales, como un campo renombrado o faltante, que las comprobaciones manuales pasarían por alto:
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
La validación de esquemas escala mejor que las aserciones campo por campo porque un esquema cubre toda la forma de la respuesta. La librería jsonschema es la elección estándar, y su documentación de validación explica las palabras clave soportadas.
Ejecutando el conjunto de pruebas en CI
Un conjunto de pruebas con pytest vale la pena cuando se ejecuta automáticamente. Pytest devuelve un código de salida no cero en caso de fallo, que es exactamente lo que un servidor de CI necesita para hacer fallar una compilación. Emite un informe JUnit para visualización en línea:
pytest -v --junitxml=results.xml
Conecta ese comando a un paso de GitHub Actions o a cualquier otra pipeline y tus pruebas de API protegerán cada commit. Nuestro tutorial sobre pruebas de API en pipelines de CI/CD muestra la configuración completa, incluyendo la inyección de secretos para tokens y la selección de entornos.
Dos hábitos de CI mantienen un conjunto de pruebas pytest fiable. Primero, nunca codifiques secretos o URLs de entorno en los archivos de prueba. Léelos de variables de entorno para que el mismo conjunto se ejecute en staging en CI y localmente sin ediciones:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Segundo, ejecuta pruebas independientes en paralelo para mantener la retroalimentación rápida. El plugin `pytest-xdist` distribuye las pruebas entre los núcleos de la CPU con `pytest -n auto`. Las ejecuciones en paralelo solo funcionan si tus pruebas no comparten estado, lo cual es una razón más por la que la capa de datos de prueba es importante. Un conjunto que depende del orden de ejecución fallará de forma impredecible en el momento en que se ejecute en paralelo.
Manteniendo un conjunto de pruebas de pytest
Un conjunto de cincuenta pruebas es fácil. Un conjunto de quinientas necesita disciplina. Tres prácticas mantienen un framework de API con pytest saludable a medida que crece.
Agrupa las pruebas relacionadas en módulos y utiliza clases solo cuando compartan la configuración, no para decorar. Un archivo test_orders.py con un conjunto claro de funciones se lee mejor que un archivo gigante. Usa marcas, registradas en pytest.ini, para etiquetar pruebas y poder ejecutar subconjuntos: @pytest.mark.smoke para una comprobación rápida, @pytest.mark.slow para un barrido completo. Ejecuta el conjunto de pruebas *smoke* en cada commit y el conjunto completo cada noche.
Centraliza la configuración. Las URL base, los esquemas y las *fixtures* compartidas pertenecen a conftest.py o a un pequeño módulo de configuración, nunca copiadas y pegadas entre archivos. Cuando la URL de *staging* cambie, solo deberías editar una línea. La misma disciplina modular que se aplica a cualquier framework, cubierta en nuestra guía sobre cómo escribir scripts de prueba automatizados, se aplica aquí: extrae cualquier cosa que escribas dos veces en una *fixture* o una función de ayuda.
Cuándo optar por una plataforma en su lugar
Un framework de pytest es excelente cuando tu equipo escribe en Python y quiere pruebas que residan junto al código de la aplicación. Es menos conveniente cuando el personal de QA o de producto necesita contribuir, o cuando quieres el diseño, el *mocking* y la ejecución de pruebas en un solo lugar sin tener que mantener código "pegamento".
Apidog cubre esa brecha. Proporciona construcción visual de pruebas, validación de esquemas contra tu especificación OpenAPI, ejecuciones basadas en datos desde CSV y JSON, y un ejecutor CLI para CI, todo sin escribir código de *fixture* y aserción a mano. Muchos equipos usan ambos: pytest para escenarios con mucha lógica y Apidog para una amplia cobertura y para diseñar y simular las API contra las que el conjunto de pruebas de pytest realiza pruebas. Puedes descargar Apidog y comparar los dos enfoques en un endpoint real en una tarde.
Preguntas frecuentes
¿Por qué usar pytest en lugar del módulo `unittest` incorporado de Python para pruebas de API?
Pytest requiere menos *boilerplate*. Las pruebas son funciones simples, las aserciones son sentencias assert simples con una salida de fallos rica, y las *fixtures* manejan la configuración de forma más flexible que los métodos basados en clases de `unittest`. Pytest también tiene un gran ecosistema de plugins y la función parametrize incorporada para pruebas basadas en datos. Aún puede ejecutar pruebas existentes al estilo `unittest`, por lo que la migración es de bajo riesgo.
¿Cuál es la diferencia entre una *fixture* y *parametrize*?
Una *fixture* proporciona un recurso reutilizable, como una sesión HTTP o un token de autenticación, a cualquier prueba que lo solicite. *Parametrize* ejecuta el mismo cuerpo de prueba varias veces contra diferentes valores de entrada. Las *fixtures* comparten la configuración; *parametrize* multiplica los casos. Se combinan bien: una prueba parametrizada aún puede depender de *fixtures*.
¿Debo hacer aserciones sobre el tiempo de respuesta en las pruebas de API con pytest?
Puedes hacerlo, utilizando response.elapsed.total_seconds(), y un límite superior laxo detecta regresiones importantes. Pero pytest es una herramienta de pruebas funcionales, no un probador de carga. Para un trabajo de rendimiento real, utiliza una herramienta dedicada. Mantén las aserciones de tiempo generosas para que la varianza normal de la red no cause fallos intermitentes.
¿Cómo mantengo las pruebas de API independientes en pytest?
Proporciona a cada prueba sus propios datos a través de *fixtures* que creen y limpien recursos, y evita depender del orden de ejecución de las pruebas. Pytest ejecuta las pruebas en orden de archivo por defecto, pero un conjunto bien diseñado no depende de eso. Las pruebas independientes pueden ejecutarse en paralelo y fallar de forma aislada, lo que facilita mucho la depuración.
¿Puede pytest validar las respuestas contra una especificación OpenAPI?
Pytest en sí no lo hace, pero puedes validar contra un esquema JSON con la librería jsonschema, y existen plugins que comprueban las respuestas contra un documento OpenAPI. Si la validación de esquemas es central para tu flujo de trabajo, una plataforma como Apidog, que valida automáticamente contra tu especificación OpenAPI, puede ahorrarte la configuración del plugin.
