11. Cobertura de clases, métodos y cambios de estado

11.1 Objetivo del tema

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.

Objetivo práctico: probar una clase verificando resultados, excepciones y estado después de cada operación.

11.2 Crear una clase de ejemplo

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.

11.3 Pruebas iniciales

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.

11.4 Medir cobertura

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.

11.5 Verificar estado interno observable

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.

11.6 Cubrir validaciones de métodos

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.

11.7 Probar métodos que modifican estado

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.

11.8 Probar transiciones de estado

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.

11.9 Probar reglas después del cambio de estado

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.

11.10 Probar estados no 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.

11.11 Medir nuevamente

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.

11.12 Qué mirar en clases

  • Métodos públicos: deberían estar ejercitados por pruebas relevantes.
  • Estado inicial: conviene verificarlo si define reglas importantes.
  • Transiciones: cuando un método cambia el estado, prueba el antes y el después.
  • Operaciones prohibidas: verifica qué ocurre cuando un método se llama en un estado inválido.

11.13 Errores frecuentes

  • Solo probar el método principal: una clase suele tener reglas repartidas en varios métodos.
  • No comprobar el estado: si un método modifica el objeto, la prueba debe verificar el efecto.
  • Ignorar estados inválidos: muchas ramas faltantes aparecen cuando el objeto está vacío, cerrado o incompleto.
  • Depender demasiado de atributos internos: verifica estado observable; si el diseño ofrece métodos públicos, prefiérelos.

11.14 Conclusión

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.