Qué resuelve una fixture
Casi cualquier test no trivial necesita preparar un contexto antes de ejecutarse: abrir una base de datos, crear un usuario de prueba, configurar un cliente HTTP, escribir un archivo temporal. Ese setup se repite en muchos tests y, si se escribe a mano en cada uno, el código se vuelve largo y frágil.
Las fixtures son funciones que producen ese contexto y pytest las inyecta automáticamente como parámetro en cada test que las necesite. Una función de test declara las fixtures que quiere recibir y pytest se encarga de crearlas, pasarlas y destruirlas después.
import pytest
@pytest.fixture
def usuario():
return {"id": 1, "nombre": "Ana"}
def test_nombre_usuario(usuario):
assert usuario["nombre"] == "Ana"
def test_id_usuario(usuario):
assert usuario["id"] == 1
Pytest detecta que ambos tests piden un parámetro llamado usuario, busca la fixture con ese nombre, la ejecuta y pasa el resultado. No hay setUp ni herencia de clase, solo una función decorada.
El mecanismo de fixtures de pytest es una forma de inyección de dependencias muy simple. Los tests piden lo que necesitan por nombre y pytest resuelve cómo obtenerlo, lo que mantiene los tests pequeños y enfocados en lo que verifican.
Liberar recursos con yield
Cuando una fixture gestiona un recurso que hay que cerrar después del test (un fichero, una conexión, un proceso), se usa yield en lugar de return. Todo lo que va después del yield se ejecuta al finalizar el test.
import pytest
import tempfile
from pathlib import Path
@pytest.fixture
def fichero_temporal():
f = tempfile.NamedTemporaryFile(delete=False, suffix=".txt")
path = Path(f.name)
f.close()
yield path # el test usa path aquí
path.unlink(missing_ok=True) # cleanup tras el test
La parte anterior al yield es el setup, el valor devuelto al test es lo que aparece como parámetro, y lo que va después es el teardown. Pytest garantiza que el teardown se ejecuta aunque el test falle.
Ámbitos: function, class, module, session
Por defecto una fixture tiene ámbito function: se crea y se destruye para cada test. Para fixtures caras (conectar a una base, arrancar un servicio), conviene reutilizar entre tests.
@pytest.fixture(scope="session")
def base_de_datos():
db = conectar_a_bd()
yield db
db.close()
Ámbitos disponibles:
function(por defecto): una instancia por test.class: una instancia por clase de tests.module: una instancia por fichero de tests.package: una instancia por paquete.session: una instancia para toda la ejecución de pytest.
Regla práctica: usa el ámbito más estrecho que funcione. Una fixture session que muta su estado entre tests introduce acoplamientos invisibles y hace que el orden de ejecución importe, algo que los tests deben evitar.
Componer fixtures
Una fixture puede declarar otras fixtures como parámetro. Pytest las resuelve recursivamente.
@pytest.fixture
def cliente_http():
return HttpClient(base_url="https://api.test")
@pytest.fixture
def usuario_autenticado(cliente_http):
token = cliente_http.post("/login", json={"user": "ana", "pw": "x"}).json()["token"]
return {"token": token, "cliente": cliente_http}
def test_perfil(usuario_autenticado):
r = usuario_autenticado["cliente"].get(
"/perfil",
headers={"Authorization": f"Bearer {usuario_autenticado['token']}"}
)
assert r.status_code == 200
usuario_autenticado depende de cliente_http. Cuando el test pide usuario_autenticado, pytest primero crea cliente_http, luego lo pasa a usuario_autenticado, y finalmente al test. El grafo de dependencias se resuelve automáticamente.
conftest.py: compartir fixtures
Las fixtures definidas en un archivo de test solo son visibles en ese archivo. Para compartir fixtures entre varios archivos, se colocan en un fichero especial llamado conftest.py.
tests/
conftest.py # fixtures visibles en tests/
test_productos.py
test_pedidos.py
api/
conftest.py # fixtures específicas del subdirectorio api/
test_endpoints.py
Cualquier test bajo el mismo directorio puede usar las fixtures del conftest.py más cercano sin importarlas manualmente. Los conftest.py anidados amplían las fixtures disponibles según la profundidad del test.
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def base_de_datos():
db = crear_bd_temporal()
yield db
db.eliminar()
@pytest.fixture
def usuario_admin(base_de_datos):
return base_de_datos.crear_usuario(rol="admin")
Ahora cualquier test puede pedir usuario_admin y pytest resolverá la cadena hasta base_de_datos.
Fixtures con parámetros: params
Una fixture puede ejecutarse varias veces con distintos valores usando params:
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def motor_bd(request):
motor = request.param
conn = conectar(motor=motor)
yield conn
conn.close()
def test_crud(motor_bd):
assert motor_bd.guardar({"id": 1}) is not None
Pytest ejecutará test_crud tres veces, una por cada valor de params. Es la forma habitual de testear el mismo comportamiento contra varios backends o configuraciones.
autouse: fixtures automáticas
Con autouse=True, la fixture se aplica sin que el test la pida explícitamente. Es útil para configuración transversal como tiempos, logging o limpieza.
@pytest.fixture(autouse=True)
def limpiar_cache():
cache.clear()
yield
cache.clear()
Se dispara antes y después de cada test del ámbito correspondiente. Úsalo con moderación: fixtures autouse que hacen "magia" pueden ocultar dependencias y complicar el diagnóstico cuando un test falla.
Fixtures incluidas por pytest
Pytest trae fixtures útiles disponibles sin declararlas:
tmp_path: directorio temporal único por test, eliminado automáticamente. Tipopathlib.Path.tmp_path_factory: factoría para crear múltiples directorios temporales.capsys/capfd: captura destdout/stderr.caplog: captura de logs.monkeypatch: para modificar atributos, variables de entorno o entradas del sistema solo durante el test.
Ejemplo con tmp_path:
def test_escribir_fichero(tmp_path):
fichero = tmp_path / "salida.txt"
fichero.write_text("hola", encoding="utf-8")
assert fichero.read_text(encoding="utf-8") == "hola"
Ejemplo con monkeypatch:
def test_con_variable_entorno(monkeypatch):
monkeypatch.setenv("API_KEY", "token-test")
assert leer_config()["api_key"] == "token-test"
monkeypatch revierte los cambios automáticamente al acabar el test, evitando fugas entre tests.
Buenas prácticas
Una fixture por responsabilidad. Separa base_de_datos, usuario, cliente_http en fixtures independientes en lugar de un megasetup. Pytest compone lo que haga falta según los parámetros de cada test.
Usa el ámbito adecuado. Recursos caros (bd, contenedores) en session o module. Recursos baratos y con estado (usuarios, datos) en function para aislar tests.
Nombres claros. El nombre de la fixture es lo que aparece en las firmas de los tests, así que debe leerse bien: usuario_admin mejor que u_admin o fix_u.
Centraliza en conftest.py. Fixtures compartidas entre varios archivos deben vivir en conftest.py. Facilita el descubrimiento y evita duplicación.
Evita estado global compartido. Si dos tests alteran la misma fixture de ámbito session, el orden importa y los tests se vuelven frágiles. Prefiere fixtures de ámbito function para lo que tenga estado mutable.
Las fixtures son el mecanismo central de pytest y la razón principal por la que se ha convertido en el framework de tests estándar en Python. Dominarlas separa el código de test mantenible del que se convierte en una pesadilla al crecer.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Python
Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Declarar fixtures con el decorador pytest.fixture. Controlar el ámbito con scope function, class, module y session. Componer fixtures pasando unas como parámetro de otras. Liberar recursos con yield. Centralizar fixtures compartidas en conftest.py.