Concepto clave
Los tests unitarios e integración son el sistema inmunológico de tu API. Imagina que cada endpoint es un órgano vital: los tests unitarios verifican que cada célula (función individual) funcione correctamente, mientras que los tests de integración aseguran que todos los órganos trabajen en armonía. En el desarrollo backend profesional, escribir tests no es opcional; es lo que separa un prototipo de un sistema en producción.
Con FastAPI, tenemos dos herramientas principales: Pytest para la infraestructura de testing y TestClient para simular peticiones HTTP sin levantar un servidor real. La magia está en que TestClient se integra directamente con tu aplicación FastAPI, permitiéndote probar rutas, dependencias, middlewares y validaciones como si estuvieras haciendo peticiones reales, pero en milisegundos y de forma aislada.
Cómo funciona en la práctica
El flujo de trabajo profesional sigue estos pasos:
- Configurar un entorno de testing separado (usualmente con una base de datos en memoria o contenedores Docker)
- Crear fixtures de Pytest para datos de prueba reutilizables
- Escribir tests unitarios para funciones puras (lógica de negocio, validadores, utilidades)
- Escribir tests de integración para endpoints completos, incluyendo autenticación y dependencias
- Ejecutar tests automáticamente en CI/CD con cobertura mínima del 80%
Un patrón común es la pirámide de testing: muchos tests unitarios rápidos (base), algunos tests de integración (medio) y pocos tests end-to-end (cima). Para APIs con FastAPI, nos enfocamos en los dos primeros niveles.
Código en acción
Veamos un ejemplo completo. Primero, una función de negocio que necesita tests unitarios:
# app/utils/calculations.py
def calculate_discount(price: float, user_tier: str) -> float:
"""Calcula descuento basado en precio y categoría de usuario"""
if user_tier == "premium":
return price * 0.8 # 20% descuento
elif user_tier == "regular" and price > 100:
return price * 0.9 # 10% descuento para compras mayores a 100
return price # Sin descuentoAhora, el test unitario correspondiente:
# tests/unit/test_calculations.py
import pytest
from app.utils.calculations import calculate_discount
def test_calculate_discount_premium():
"""Test: usuario premium recibe 20% de descuento siempre"""
result = calculate_discount(200.0, "premium")
assert result == 160.0 # 200 * 0.8
def test_calculate_discount_regular_large_purchase():
"""Test: usuario regular con compra >100 recibe 10% descuento"""
result = calculate_discount(150.0, "regular")
assert result == 135.0 # 150 * 0.9
def test_calculate_discount_no_discount():
"""Test: usuario regular con compra pequeña no recibe descuento"""
result = calculate_discount(50.0, "regular")
assert result == 50.0
# Test con parametrización (patrón profesional)
@pytest.mark.parametrize("price,user_tier,expected", [
(200, "premium", 160),
(150, "regular", 135),
(50, "regular", 50),
(80, "premium", 64),
])
def test_calculate_discount_parametrized(price, user_tier, expected):
"""Test múltiples casos en una sola función"""
result = calculate_discount(float(price), user_tier)
assert result == expectedErrores comunes
- Testear implementación en lugar de comportamiento: Escribir tests que dependen de detalles internos en lugar de lo que la función debería hacer. Si refactorizas, los tests se rompen aunque el comportamiento sea correcto.
- No aislar tests de integración: Usar la misma base de datos de desarrollo para tests, contaminando datos entre ejecuciones. Usa bases de datos en memoria (SQLite) o contenedores efímeros.
- Olvidar resetear el estado entre tests: Si un test modifica variables globales o estado compartido, puede afectar tests siguientes. Usa fixtures de Pytest con scope="function".
- Tests demasiado específicos: Assert que comparan strings completos o estructuras exactas en lugar de verificar los aspectos clave. Esto hace los tests frágiles ante cambios menores.
- No probar casos edge: Solo testear el "happy path". Los bugs suelen aparecer en entradas inesperadas, valores límite o errores de red.
Checklist de dominio
- ✓ Puedo escribir tests unitarios para funciones puras usando pytest.mark.parametrize
- ✓ Configuro TestClient correctamente, sobrescribiendo dependencias para testing
- ✓ Uso fixtures de Pytest para crear datos de prueba reutilizables y aislados
- ✓ Pruebo respuestas HTTP, códigos de estado, schemas de respuesta y errores
- ✓ Mido cobertura de código y mantengo mínimo 80% en lógica de negocio
- ✓ Integro tests en pipeline CI/CD que falla si no pasan
- ✓ Escribo tests para autenticación, autorización y middlewares
Implementar tests para API de gestión de productos con autenticación
En este ejercicio práctico, implementarás tests completos para una API REST de productos que incluye autenticación JWT. Sigue estos pasos:
- Configuración inicial: Crea un archivo
tests/conftest.pycon fixtures para:- TestClient con app FastAPI
- Base de datos SQLite en memoria para testing
- Usuario de prueba con token JWT válido
- Tests unitarios: Escribe tests para:
- Función que valida precio de producto (debe ser positivo)
- Función que formatea respuesta de productos
- Tests de integración: Usando TestClient, prueba:
- GET /products sin autenticación (debe devolver 401)
- GET /products con token válido (debe devolver lista)
- POST /products con datos válidos (crea producto)
- POST /products con precio negativo (debe devolver 422)
- DELETE /products/{id} con usuario no autorizado (debe devolver 403)
- Refactorización: Identica código duplicado en los tests y extraelo a fixtures o funciones helper.
- Ejecución: Corre tests con
pytest -v --cov=app --cov-report=htmly verifica cobertura >85%.
- Usa pytest.fixture para crear un token JWT de prueba que expire en 30 minutos
- Para testear respuestas de error, verifica tanto el status_code como el JSON de respuesta
- Crea una fixture que limpie la base de datos después de cada test usando yield
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.