12. Fixtures reutilizables para preparar datos, objetos y estados iniciales

12.1 Objetivo del tema

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.

Objetivo práctico: crear fixtures con @pytest.fixture y usarlas en varias pruebas sin duplicar preparación.

12.2 Qué es una fixture

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.

12.3 Primera fixture práctica

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"

12.4 Cómo pytest inyecta fixtures

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.

12.5 Ejecutar las pruebas con fixtures

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.

12.6 Evitar duplicación con fixtures

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.

12.7 Fixtures para distintos datos

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

12.8 Fixtures que devuelven listas

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

12.9 Fixtures que dependen de otras fixtures

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.

12.10 No modificar fixtures compartidas sin cuidado

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.

12.11 Fixtures con yield para preparación y limpieza

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.

12.12 Ejemplo con archivo temporal

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.

12.13 Nombres claros para fixtures

Una fixture debe nombrarse por lo que entrega:

  • usuario_valido
  • usuario_inactivo
  • productos_carrito
  • archivo_saludo
  • carrito_con_producto

Evita nombres como data, fixture1, obj o preparacion, porque no explican qué recibe la prueba.

12.14 Cuándo usar fixtures

Conviene usar fixtures cuando:

  • La preparación se repite en varias pruebas.
  • El dato tiene un significado claro dentro del dominio.
  • La prueba queda más legible al recibir el dato preparado.
  • Hay que crear y limpiar recursos.
  • Varias pruebas necesitan el mismo estado inicial.

12.15 Cuándo no usar fixtures

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.

12.16 Marcadores y fixtures

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.

12.17 Ejecutar solo pruebas que usan una fixture

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.

12.18 Problemas frecuentes

  • Fixture no encontrada: revisa que el nombre del parámetro coincida exactamente con el nombre de la fixture.
  • La fixture no se ejecuta: verifica que la prueba la reciba como parámetro.
  • Se repite demasiada preparación: considera extraer una fixture.
  • Hay demasiadas fixtures pequeñas: deja dentro de la prueba los datos simples que no se reutilizan.
  • Una prueba se vuelve difícil de leer: revisa si la fixture oculta demasiada información importante.

12.19 Ejercicio práctico

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:

  • Un producto sin descuento.
  • Un producto con 10% de descuento.
  • Un producto con 100% de descuento.

Automatiza una prueba para cada caso.

12.20 Solución propuesta

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

12.21 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Sabes crear fixtures con @pytest.fixture.
  • Sabes recibir fixtures como parámetros en pruebas.
  • Usas nombres claros para las fixtures.
  • Evitas duplicar datos de preparación en varias pruebas.
  • Usas yield cuando hace falta limpiar recursos.
  • No conviertes cada dato simple en fixture sin necesidad.
  • La suite se ejecuta correctamente con python -m pytest.

12.22 Conclusión

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.