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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
Ejecuta:
python -m pytest
La salida esperada será similar a:
collected 6 items
test_carrito.py ...... [100%]
6 passed in 0.03s
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.
| 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. |
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.
mkdir pytest-fixtures-demo
cd pytest-fixtures-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest --fixtures
@pytest.fixture.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.