Cuando probamos funciones puras, normalmente verificamos entradas y salidas. Con clases aparece otro aspecto: el estado interno del objeto puede cambiar después de llamar a un método.
En este tema vamos a usar cobertura para revisar métodos no ejecutados, ramas sin probar y cambios de estado que deben verificarse explícitamente.
Crea el archivo src/tienda/carrito.py:
class Carrito:
def __init__(self):
self.items = []
self.cerrado = False
def agregar(self, producto, precio, cantidad=1):
if self.cerrado:
raise RuntimeError("No se puede modificar un carrito cerrado")
if precio <= 0:
raise ValueError("El precio debe ser mayor que cero")
if cantidad <= 0:
raise ValueError("La cantidad debe ser mayor que cero")
self.items.append({
"producto": producto,
"precio": precio,
"cantidad": cantidad,
})
def total(self):
return sum(item["precio"] * item["cantidad"] for item in self.items)
def vaciar(self):
if self.cerrado:
raise RuntimeError("No se puede vaciar un carrito cerrado")
self.items.clear()
def cerrar(self):
if not self.items:
raise RuntimeError("No se puede cerrar un carrito vacío")
self.cerrado = True
La clase tiene estado en items y cerrado. Los métodos no solo devuelven valores: también modifican el objeto.
Una primera prueba puede verificar que agregar productos cambia el total:
from tienda.carrito import Carrito
def test_carrito_calcula_total():
carrito = Carrito()
carrito.agregar("Libro", 1000, 2)
carrito.agregar("Lápiz", 100, 3)
assert carrito.total() == 2300
Esta prueba ejecuta __init__, agregar y total, pero todavía deja sin cubrir validaciones y otros cambios de estado.
Ejecuta el reporte con líneas faltantes.
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-report=term-missing
Una salida posible para carrito.py sería:
Name Stmts Miss Cover Missing
-----------------------------------------------------
src\tienda\carrito.py 25 9 64% 7, 10, 13, 27, 29, 32, 35, 37, 39
Las líneas faltantes suelen corresponder a validaciones, métodos no llamados y estados que todavía no fueron ejercitados.
Después de llamar a un método que modifica el objeto, la prueba debe comprobar el cambio relevante.
def test_agregar_producto_guarda_item():
carrito = Carrito()
carrito.agregar("Libro", 1000, 2)
assert carrito.items == [
{"producto": "Libro", "precio": 1000, "cantidad": 2}
]
Esta prueba mira el estado del carrito después de la operación. En clases, esa verificación suele ser tan importante como el valor devuelto.
Las validaciones se prueban igual que en funciones, pero creando primero el objeto:
import pytest
from tienda.carrito import Carrito
def test_agregar_rechaza_precio_invalido():
carrito = Carrito()
with pytest.raises(ValueError):
carrito.agregar("Libro", 0)
def test_agregar_rechaza_cantidad_invalida():
carrito = Carrito()
with pytest.raises(ValueError):
carrito.agregar("Libro", 1000, 0)
Estas pruebas cubren caminos de error y confirman que el objeto no acepta datos inválidos.
El método vaciar no devuelve un valor, pero modifica items. La prueba debe verificar ese efecto:
def test_vaciar_elimina_items():
carrito = Carrito()
carrito.agregar("Libro", 1000)
carrito.vaciar()
assert carrito.items == []
assert carrito.total() == 0
Cuando un método devuelve None, normalmente la prueba debe mirar el estado resultante o una colaboración observable.
El método cerrar cambia el carrito a un estado donde ya no puede modificarse:
def test_cerrar_carrito_cambia_estado():
carrito = Carrito()
carrito.agregar("Libro", 1000)
carrito.cerrar()
assert carrito.cerrado is True
La prueba cubre la transición, pero todavía falta comprobar la consecuencia de esa transición.
Una vez cerrado, el carrito no debería aceptar más modificaciones:
def test_no_se_puede_agregar_a_carrito_cerrado():
carrito = Carrito()
carrito.agregar("Libro", 1000)
carrito.cerrar()
with pytest.raises(RuntimeError):
carrito.agregar("Lápiz", 100)
def test_no_se_puede_vaciar_carrito_cerrado():
carrito = Carrito()
carrito.agregar("Libro", 1000)
carrito.cerrar()
with pytest.raises(RuntimeError):
carrito.vaciar()
Estas pruebas verifican una regla de estado: después de cerrar el carrito, ciertos métodos dejan de estar permitidos.
También hay que probar que no se pueda cerrar un carrito vacío:
def test_no_se_puede_cerrar_carrito_vacio():
carrito = Carrito()
with pytest.raises(RuntimeError):
carrito.cerrar()
Este caso no tiene que ver con una entrada numérica, sino con el estado actual del objeto.
Vuelve a ejecutar:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-report=term-missing
La cobertura debería subir porque ahora se ejecutan métodos, validaciones y transiciones que antes no estaban cubiertas.
En este tema usamos cobertura para revisar clases, métodos y cambios de estado. Las pruebas no solo comprobaron resultados, sino también transiciones y operaciones no permitidas.
En el próximo tema vamos a discutir la diferencia entre cobertura de sentencias y calidad real de las pruebas.