17. Fixtures: preparación reutilizable de datos y objetos

17.1 Objetivo del tema

Muchas pruebas necesitan preparar los mismos datos antes de ejecutar una comprobación. Si repetimos esa preparación en cada prueba, el archivo crece y se vuelve más difícil de mantener.

Las fixtures de pytest permiten definir datos u objetos reutilizables. Una prueba los pide por nombre y pytest se encarga de crearlos antes de ejecutar la prueba.

Idea clave: una fixture prepara algo que varias pruebas necesitan, sin duplicar código de preparación.

17.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-fixtures-demo
cd pytest-fixtures-demo

Si pytest no está instalado en el entorno activo:

python -m pip install pytest

17.3 Crear el código a probar

Crea un archivo llamado carrito.py:

class Carrito:
    def __init__(self):
        self.items = []

    def agregar(self, producto, cantidad=1):
        if cantidad <= 0:
            raise ValueError("La cantidad debe ser mayor que cero")

        self.items.append({
            "producto": producto,
            "cantidad": cantidad,
        })

    def cantidad_total(self):
        return sum(item["cantidad"] for item in self.items)

    def subtotal(self):
        total = 0
        for item in self.items:
            total += item["producto"]["precio"] * item["cantidad"]
        return total

    def esta_vacio(self):
        return len(self.items) == 0

La clase Carrito tiene estado interno, por lo que varias pruebas necesitarán crear productos y carritos.

17.4 Pruebas sin fixture

Crea test_carrito.py con algunas pruebas directas:

import pytest

from carrito import Carrito


def test_carrito_nuevo_esta_vacio():
    carrito = Carrito()

    assert carrito.esta_vacio() is True


def test_agregar_producto_incrementa_cantidad():
    carrito = Carrito()
    producto = {"nombre": "Teclado", "precio": 50000}

    carrito.agregar(producto, cantidad=2)

    assert carrito.cantidad_total() == 2


def test_subtotal_con_un_producto():
    carrito = Carrito()
    producto = {"nombre": "Teclado", "precio": 50000}

    carrito.agregar(producto, cantidad=2)

    assert carrito.subtotal() == 100000

Funciona, pero repetimos Carrito() y el mismo producto en varias pruebas.

17.5 Crear una primera fixture

Una fixture se define con @pytest.fixture:

import pytest

from carrito import Carrito


@pytest.fixture
def carrito():
    return Carrito()

Ahora cualquier prueba puede pedir carrito como argumento.

17.6 Usar una fixture en una prueba

La prueba recibe la fixture por nombre:

def test_carrito_nuevo_esta_vacio(carrito):
    assert carrito.esta_vacio() is True

No llamamos manualmente a carrito(). pytest detecta el nombre del argumento, ejecuta la fixture y entrega su resultado.

17.7 Crear fixture para producto

Agrega una fixture para el producto:

@pytest.fixture
def producto_teclado():
    return {"nombre": "Teclado", "precio": 50000}

Ahora podemos reutilizar el mismo producto en varias pruebas.

17.8 Usar varias fixtures en una prueba

Una prueba puede pedir varias fixtures:

def test_agregar_producto_incrementa_cantidad(carrito, producto_teclado):
    carrito.agregar(producto_teclado, cantidad=2)

    assert carrito.cantidad_total() == 2

El orden de los argumentos no cambia el resultado, pero conviene mantener un orden legible.

17.9 Fixture que depende de otra fixture

Una fixture también puede pedir otra fixture:

@pytest.fixture
def carrito_con_teclado(carrito, producto_teclado):
    carrito.agregar(producto_teclado, cantidad=2)
    return carrito

Esta fixture devuelve un carrito ya preparado con datos iniciales.

17.10 Usar una fixture preparada

Ahora la prueba puede concentrarse en la comprobación:

def test_subtotal_con_un_producto(carrito_con_teclado):
    assert carrito_con_teclado.subtotal() == 100000

La preparación quedó aislada en la fixture.

17.11 Cada prueba recibe una instancia nueva

Por defecto, una fixture se ejecuta una vez por prueba. Eso evita que una prueba contamine a otra:

def test_carrito_sigue_vacio_en_otra_prueba(carrito):
    assert carrito.cantidad_total() == 0

Aunque otra prueba haya agregado productos, esta recibe un carrito nuevo.

17.12 Probar errores usando fixtures

Las fixtures también sirven para pruebas de excepciones:

def test_agregar_cantidad_cero_lanza_error(carrito, producto_teclado):
    with pytest.raises(ValueError):
        carrito.agregar(producto_teclado, cantidad=0)

El código de preparación no se repite y la prueba queda enfocada en el error esperado.

17.13 Fixture para varios productos

Podemos preparar una lista de productos:

@pytest.fixture
def productos():
    return [
        {"nombre": "Teclado", "precio": 50000},
        {"nombre": "Mouse", "precio": 12000},
    ]

Luego la usamos en una prueba:

def test_subtotal_con_varios_productos(carrito, productos):
    carrito.agregar(productos[0], cantidad=1)
    carrito.agregar(productos[1], cantidad=2)

    assert carrito.subtotal() == 74000

17.14 Archivo completo de pruebas

El archivo test_carrito.py puede quedar así:

import pytest

from carrito import Carrito


@pytest.fixture
def carrito():
    return Carrito()


@pytest.fixture
def producto_teclado():
    return {"nombre": "Teclado", "precio": 50000}


@pytest.fixture
def productos():
    return [
        {"nombre": "Teclado", "precio": 50000},
        {"nombre": "Mouse", "precio": 12000},
    ]


@pytest.fixture
def carrito_con_teclado(carrito, producto_teclado):
    carrito.agregar(producto_teclado, cantidad=2)
    return carrito


def test_carrito_nuevo_esta_vacio(carrito):
    assert carrito.esta_vacio() is True


def test_agregar_producto_incrementa_cantidad(carrito, producto_teclado):
    carrito.agregar(producto_teclado, cantidad=2)

    assert carrito.cantidad_total() == 2


def test_subtotal_con_un_producto(carrito_con_teclado):
    assert carrito_con_teclado.subtotal() == 100000


def test_carrito_sigue_vacio_en_otra_prueba(carrito):
    assert carrito.cantidad_total() == 0


def test_agregar_cantidad_cero_lanza_error(carrito, producto_teclado):
    with pytest.raises(ValueError):
        carrito.agregar(producto_teclado, cantidad=0)


def test_subtotal_con_varios_productos(carrito, productos):
    carrito.agregar(productos[0], cantidad=1)
    carrito.agregar(productos[1], cantidad=2)

    assert carrito.subtotal() == 74000

17.15 Ejecutar las pruebas

Ejecuta:

python -m pytest

La salida esperada será similar a:

collected 6 items

test_carrito.py ......                                           [100%]

6 passed in 0.03s

17.16 Ver fixtures disponibles

pytest puede mostrar las fixtures disponibles:

python -m pytest --fixtures

La lista incluye fixtures propias y fixtures internas de pytest. En proyectos grandes puede ser bastante extensa.

17.17 Cuándo conviene usar fixtures

Conviene usar fixture Puede no hacer falta
Varias pruebas repiten la misma preparación. La preparación es una sola línea simple y no se repite.
Necesitamos crear objetos con estado. La prueba es más clara con datos escritos directamente.
Queremos aislar preparación de verificación. La fixture ocultaría detalles importantes del caso.

17.18 Nombres claros para fixtures

Una fixture debe tener un nombre que explique qué entrega:

@pytest.fixture
def carrito_con_teclado():
    ...


@pytest.fixture
def producto_teclado():
    ...

Evita nombres demasiado genéricos como data, obj o fixture1.

17.19 Errores frecuentes

  • Llamar la fixture manualmente: en una prueba se pide por argumento, no se llama como función.
  • Usar nombres poco claros: dificulta entender qué datos recibe la prueba.
  • Crear fixtures demasiado grandes: pueden ocultar la preparación importante.
  • Modificar datos compartidos sin cuidado: por defecto cada prueba recibe una ejecución nueva, pero los objetos mutables siempre requieren atención.
  • Usar fixtures para todo: si un dato solo se usa una vez, puede ser más claro escribirlo dentro de la prueba.

17.20 Comandos usados en este tema

mkdir pytest-fixtures-demo
cd pytest-fixtures-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest --fixtures

17.21 Qué debes recordar de este tema

  • Una fixture se define con @pytest.fixture.
  • Las pruebas reciben fixtures como argumentos.
  • Las fixtures ayudan a reutilizar preparación de datos y objetos.
  • Una fixture puede depender de otra fixture.
  • Por defecto, cada prueba recibe una ejecución nueva de la fixture.
  • Los nombres claros son fundamentales para leer las pruebas.

17.22 Conclusión

En este tema aprendimos a usar fixtures para preparar datos y objetos reutilizables con pytest. Vimos cómo pedir fixtures por nombre, cómo combinar varias y cómo crear fixtures que dependen de otras.

En el próximo tema veremos el alcance de fixtures y cómo reutilizarlas desde conftest.py.