Muchas pruebas necesitan preparar datos antes de ejecutar una verificación. Si esa preparación se repite en varios lugares, la suite se vuelve más larga, más frágil y más difícil de mantener.
En este tema usaremos fixtures de pytest para preparar datos, objetos y estados iniciales de forma reutilizable.
Una fixture es una función especial que prepara algo que una prueba necesita. Puede devolver datos simples, objetos, archivos temporales, configuraciones o cualquier recurso necesario para probar.
En pytest se define con @pytest.fixture:
import pytest
@pytest.fixture
def usuario_valido():
return {
"nombre": "Ana",
"email": "ana@example.com",
"activo": True,
}
La prueba puede recibir esa fixture como parámetro.
Crea el archivo app/usuarios.py si todavía no existe:
def usuario_esta_activo(usuario):
return usuario["activo"] is True
def obtener_email(usuario):
return usuario["email"].strip().lower()
Luego crea tests/test_usuarios_fixtures.py:
import pytest
from app.usuarios import obtener_email, usuario_esta_activo
@pytest.fixture
def usuario_valido():
return {
"nombre": "Ana",
"email": " ANA@EXAMPLE.COM ",
"activo": True,
}
def test_usuario_esta_activo_con_usuario_activo_devuelve_true(usuario_valido):
assert usuario_esta_activo(usuario_valido) is True
def test_obtener_email_devuelve_email_normalizado(usuario_valido):
assert obtener_email(usuario_valido) == "ana@example.com"
pytest detecta que la prueba tiene un parámetro llamado usuario_valido. Luego busca una fixture con ese nombre, la ejecuta y pasa el resultado a la prueba.
def test_obtener_email_devuelve_email_normalizado(usuario_valido):
assert obtener_email(usuario_valido) == "ana@example.com"
No llamamos manualmente a usuario_valido(). pytest se encarga de hacerlo.
Ejecuta el archivo nuevo:
python -m pytest tests/test_usuarios_fixtures.py
También puedes ejecutar la suite completa:
python -m pytest
Si todo está correcto, ambas pruebas deberían pasar.
Sin fixture, podríamos repetir el mismo diccionario en cada prueba:
def test_usuario_esta_activo():
usuario = {
"nombre": "Ana",
"email": " ANA@EXAMPLE.COM ",
"activo": True,
}
assert usuario_esta_activo(usuario) is True
Con fixture, la preparación queda en un solo lugar y las pruebas se enfocan en la verificación.
Podemos crear una fixture para un usuario inactivo:
@pytest.fixture
def usuario_inactivo():
return {
"nombre": "Luis",
"email": "luis@example.com",
"activo": False,
}
Y usarla en una prueba:
def test_usuario_esta_activo_con_usuario_inactivo_devuelve_false(usuario_inactivo):
assert usuario_esta_activo(usuario_inactivo) is False
También podemos preparar colecciones completas:
@pytest.fixture
def productos_carrito():
return [
{"precio": 100, "cantidad": 2},
{"precio": 50, "cantidad": 3},
]
Uso en una prueba:
from app.carrito import calcular_total
def test_calcular_total_con_productos_de_fixture_devuelve_suma_total(productos_carrito):
assert calcular_total(productos_carrito) == 350
Una fixture puede recibir otra fixture como parámetro:
@pytest.fixture
def producto_base():
return {"precio": 100, "cantidad": 1}
@pytest.fixture
def carrito_con_producto(producto_base):
return [producto_base]
Esto permite armar preparaciones por capas, sin duplicar datos.
Si una prueba modifica un diccionario o lista recibido desde una fixture, puede generar confusión. Por defecto, pytest ejecuta la fixture una vez por prueba, pero aun así conviene mantener cada prueba clara.
Ejemplo aceptable:
def test_usuario_puede_cambiar_a_inactivo(usuario_valido):
usuario_valido["activo"] = False
assert usuario_esta_activo(usuario_valido) is False
La modificación pertenece a esa prueba. Si varias pruebas necesitan ese estado, crea una fixture específica.
Algunas fixtures necesitan preparar algo antes de la prueba y limpiarlo después. Para eso usamos yield.
import pytest
@pytest.fixture
def recurso_temporal():
recurso = {"abierto": True}
yield recurso
recurso["abierto"] = False
El código antes de yield prepara el recurso. El código después de yield se ejecuta al finalizar la prueba.
pytest incluye una fixture llamada tmp_path para crear archivos temporales. Crea app/archivos.py:
def leer_texto(ruta):
return ruta.read_text(encoding="utf-8")
Crea la prueba:
import pytest
from app.archivos import leer_texto
@pytest.fixture
def archivo_saludo(tmp_path):
ruta = tmp_path / "saludo.txt"
ruta.write_text("Hola pytest", encoding="utf-8")
return ruta
def test_leer_texto_devuelve_contenido_del_archivo(archivo_saludo):
assert leer_texto(archivo_saludo) == "Hola pytest"
tmp_path crea una carpeta temporal aislada para la prueba.
Una fixture debe nombrarse por lo que entrega:
usuario_validousuario_inactivoproductos_carritoarchivo_saludocarrito_con_productoEvita nombres como data, fixture1, obj o preparacion, porque no explican qué recibe la prueba.
Conviene usar fixtures cuando:
No hace falta crear una fixture para cada valor pequeño. Si el dato solo se usa una vez y es fácil de leer, puede quedar dentro de la prueba.
Ejemplo suficiente sin fixture:
def test_validar_cupon_con_codigo_correcto_devuelve_true():
assert validar_cupon("DESC10") is True
Crear fixtures innecesarias puede hacer que la prueba sea más difícil de seguir.
Las fixtures pueden usarse junto con marcadores sin problema:
import pytest
@pytest.mark.carrito
@pytest.mark.critica
def test_calcular_total_con_productos_de_fixture_devuelve_suma_total(productos_carrito):
assert calcular_total(productos_carrito) == 350
La fixture prepara el dato. Los marcadores clasifican la prueba.
pytest no selecciona directamente por fixture desde la línea de comandos. Pero si los nombres son claros, puedes usar -k:
python -m pytest -k productos_carrito
Otra alternativa es marcar las pruebas que usan cierto tipo de dato, por ejemplo con @pytest.mark.carrito.
Crea fixtures para probar una función de descuentos. Primero crea app/descuentos.py:
def aplicar_descuento(producto):
precio = producto["precio"]
descuento = producto["descuento"]
return precio - (precio * descuento / 100)
Luego crea tests/test_descuentos_fixtures.py con fixtures para:
Automatiza una prueba para cada caso.
Archivo tests/test_descuentos_fixtures.py:
import pytest
from app.descuentos import aplicar_descuento
@pytest.fixture
def producto_sin_descuento():
return {"precio": 100, "descuento": 0}
@pytest.fixture
def producto_con_descuento():
return {"precio": 100, "descuento": 10}
@pytest.fixture
def producto_gratis():
return {"precio": 100, "descuento": 100}
def test_aplicar_descuento_con_descuento_cero_devuelve_precio_original(producto_sin_descuento):
assert aplicar_descuento(producto_sin_descuento) == 100
def test_aplicar_descuento_con_diez_por_ciento_devuelve_precio_reducido(producto_con_descuento):
assert aplicar_descuento(producto_con_descuento) == 90
def test_aplicar_descuento_con_cien_por_ciento_devuelve_cero(producto_gratis):
assert aplicar_descuento(producto_gratis) == 0
Ejecuta:
python -m pytest tests/test_descuentos_fixtures.py
Antes de continuar con el próximo tema, verifica lo siguiente:
@pytest.fixture.yield cuando hace falta limpiar recursos.python -m pytest.En este tema usamos fixtures para preparar datos, objetos y estados iniciales. Las fixtures ayudan a reducir duplicación, mejorar la legibilidad y mantener una suite automatizada más ordenada.
En el próximo tema veremos conftest.py, que permite compartir fixtures entre varios archivos de prueba sin importarlas manualmente.