Yeda AI Tips · #079

English

Un Fixture, Tres Bases de Datos

Un decorador. Tres bases de datos. Cero tests extra. Escribiste una suite sólida contra SQLite porque es rápida y no necesita configuración — pero producción corre PostgreSQL, y la brecha entre ambas es exactamente donde se esconden los bugs. pytest cierra esa brecha con la parametrización de fixtures: marca un solo fixture con una lista de backends, y cada test que depende de él se multiplica en silencio.

El mecanismo

Un fixture de pytest acepta una lista params. Cuando la tiene, pytest llama al fixture una vez por parámetro y vuelve a ejecutar todo el conjunto de tests dependientes cada vez. El valor actual llega a través del fixture integrado request como request.param:

import pytest
from sqlalchemy import create_engine

DB_URLS = [
    "sqlite://",                                      # in-memory
    "postgresql+psycopg2://app:secret@localhost/app_test",
    "mysql+pymysql://app:secret@localhost/app_test",
]

@pytest.fixture(params=DB_URLS, ids=["sqlite", "postgres", "mysql"])
def db_engine(request):
    engine = create_engine(request.param)
    yield engine
    engine.dispose()

def test_saves_order(db_engine):
    ...  # escrito una vez, corre tres veces

test_saves_order aparece en el reporte como tres tests — test_saves_order[sqlite], [postgres], [mysql] — sin cambiar una línea del cuerpo del test. La documentación de pytest lo dice directamente: params es "una lista opcional de parámetros que causará múltiples invocaciones de la función fixture y de todos los tests que la usan".

Por qué la suite solo-SQLite te miente

SQLite es famosamente permisiva. Diferencias que pasan en local y explotan en producción:

Comportamiento de SQLiteComportamiento de PostgreSQL / MySQL
Tipado débil — guarda "abc" en una columna INTEGERTipos estrictos; el insert falla
LIKE insensible a mayúsculas para ASCII por defectoEn PostgreSQL, LIKE distingue mayúsculas
Manejo laxo de fechas (strings)Tipos DATE/TIMESTAMP reales, errores reales
ALTER TABLE limitadoDDL completo — las migraciones se comportan distinto

Cualquiera de estas es un test verde en tu laptop y rojo en producción. Correr las mismas aserciones contra el motor real atrapa toda la clase de errores de una vez.

Reglas prácticas

Jugadas avanzadas

Salta lo que no está instalado. Envuelve un parámetro en pytest.param(...) para adjuntarle marks — el mismo truco que la documentación muestra para fixtures parametrizados:

@pytest.fixture(params=[
    "sqlite://",
    pytest.param(PG_URL, marks=pytest.mark.skipif(not has_pg(), reason="no postgres")),
])
def db_engine(request):
    ...

En local la suite degrada con gracia a SQLite; en CI, donde los servicios existen, corren todos los backends.

Apila parametrización. Un fixture parametrizado se compone con @pytest.mark.parametrize en el test: 3 backends x 4 casos de entrada = 12 tests generados, todavía una sola función.

Apúntalo a servicios reales. Al cuerpo del fixture no le importa de dónde viene la URL — un servicio de docker compose, una variable de entorno o un contenedor descartable. El patrón es idéntico: un fixture, un request.param, N backends.

Recursos

Read this article in English

¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.

Habla con nosotros · Lee el blog