12. Pruebas sobre cambios de estado

12.1 Introducción

En el tema anterior vimos pruebas sobre valores de retorno. Pero no todas las unidades devuelven toda la información importante. Muchas unidades modifican el estado de un objeto.

Un objeto puede cambiar su saldo, su lista de ítems, su estado de aprobación, su contador interno o sus propiedades. En esos casos, la prueba debe verificar cómo quedó el objeto después de ejecutar una acción.

En este tema veremos cómo probar cambios de estado de forma clara, cómo identificar el estado inicial y final, y qué errores conviene evitar.

12.2 Qué es el estado

El estado de un objeto es el conjunto de datos que el objeto conserva en un momento determinado. Por ejemplo, una cuenta bancaria puede tener un saldo; un carrito puede tener una lista de productos; un pedido puede tener un estado como pendiente, aprobado o cancelado.

Ejemplo simple:

class Cuenta:
    def __init__(self, saldo):
        self.saldo = saldo

En este caso, saldo forma parte del estado de la cuenta. Si un método modifica ese saldo, podemos escribir una prueba sobre cambio de estado.

12.3 Estructura de una prueba de estado

Una prueba sobre cambios de estado suele seguir esta forma:

  • Estado inicial: cómo está el objeto antes de la acción.
  • Acción: qué operación se ejecuta.
  • Estado final esperado: cómo debería quedar el objeto.
Probar estado significa comparar el objeto después de la acción con el estado que esperamos observar.

12.4 Primer ejemplo: depositar dinero

Veamos una cuenta con un método para depositar dinero.

class Cuenta:
    def __init__(self, saldo):
        self.saldo = saldo

    def depositar(self, importe):
        self.saldo += importe


def test_depositar_incrementa_el_saldo():
    cuenta = Cuenta(100)

    cuenta.depositar(50)

    assert cuenta.saldo == 150

La prueba verifica un cambio de estado:

  • Estado inicial: saldo 100.
  • Acción: depositar 50.
  • Estado final esperado: saldo 150.

12.5 Diferencia con probar valores de retorno

En una prueba sobre retorno, la información importante vuelve como resultado de la función. En una prueba sobre estado, la información importante queda guardada dentro del objeto.

Tipo de prueba Qué se verifica Ejemplo
Valor de retorno Lo que devuelve la función. assert calcular_total(items) == 300
Cambio de estado Cómo quedó el objeto después de una acción. assert cuenta.saldo == 150

12.6 Probar extracción de dinero

Otro comportamiento de una cuenta es extraer dinero. La prueba debe verificar que el saldo disminuye correctamente.

class Cuenta:
    def __init__(self, saldo):
        self.saldo = saldo

    def extraer(self, importe):
        self.saldo -= importe


def test_extraer_disminuye_el_saldo():
    cuenta = Cuenta(100)

    cuenta.extraer(30)

    assert cuenta.saldo == 70

La prueba no necesita que extraer devuelva un valor. La verificación se hace observando el saldo final.

12.7 Probar listas internas

Un carrito de compras suele mantener una lista de ítems. Agregar un ítem modifica esa lista.

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

    def agregar_item(self, item):
        self.items.append(item)


def test_agregar_item_lo_incorpora_al_carrito():
    carrito = Carrito()

    carrito.agregar_item("teclado")

    assert carrito.items == ["teclado"]

El estado inicial es una lista vacía. El estado final esperado es una lista con el ítem agregado.

12.8 Probar acumulación de estado

Algunas unidades acumulan información a lo largo de varias operaciones. En una prueba unitaria conviene mantener el caso simple, pero puede ser necesario ejecutar más de una acción si el comportamiento probado es acumulativo.

def test_agregar_dos_items_los_conserva_en_orden():
    carrito = Carrito()

    carrito.agregar_item("teclado")
    carrito.agregar_item("mouse")

    assert carrito.items == ["teclado", "mouse"]

Aquí las dos acciones forman parte del mismo comportamiento: acumular ítems en orden. No estamos mezclando reglas diferentes.

12.9 Probar cambios de estado con reglas

Los cambios de estado suelen estar protegidos por reglas. Por ejemplo, un pedido puede pasar de pendiente a aprobado solo si cumple ciertas condiciones.

class Pedido:
    def __init__(self, total):
        self.total = total
        self.estado = "pendiente"

    def aprobar(self):
        if self.total > 0:
            self.estado = "aprobado"


def test_aprobar_pedido_con_total_positivo_cambia_estado_a_aprobado():
    pedido = Pedido(total=100)

    pedido.aprobar()

    assert pedido.estado == "aprobado"

La prueba verifica que una acción cambia el estado solo bajo una condición válida.

12.10 Probar que el estado no cambia

A veces el comportamiento esperado es que el estado permanezca igual. Esto también debe probarse cuando la regla es importante.

def test_aprobar_pedido_con_total_cero_mantiene_estado_pendiente():
    pedido = Pedido(total=0)

    pedido.aprobar()

    assert pedido.estado == "pendiente"

Esta prueba protege el caso negativo: un pedido con total cero no debe aprobarse.

12.11 Verificar solo el estado relevante

Cuando un objeto tiene muchas propiedades, no siempre conviene verificar todas. La prueba debe enfocarse en el estado relevante para el comportamiento probado.

def test_cancelar_pedido_cambia_estado_a_cancelado():
    pedido = Pedido(total=100)

    pedido.cancelar()

    assert pedido.estado == "cancelado"

Si el objetivo es probar la cancelación, quizá no hace falta verificar el total, la fecha, el cliente y otros datos que no deberían cambiar. Agregar aserciones innecesarias puede volver la prueba frágil.

12.12 Estado visible y estado interno

Conviene verificar el estado observable desde la interfaz pública del objeto. Si una propiedad es parte del contrato de uso, puede comprobarse. Si es un detalle interno, quizá no convenga acoplar la prueba a ella.

Por ejemplo, si una clase ofrece un método obtener_saldo(), puede ser mejor usarlo en la prueba en lugar de leer una variable interna privada.

def test_depositar_incrementa_el_saldo_visible():
    cuenta = Cuenta(100)

    cuenta.depositar(50)

    assert cuenta.obtener_saldo() == 150

Esto ayuda a probar el comportamiento desde la perspectiva de quien usa la clase.

12.13 Evitar pruebas demasiado acopladas

Una prueba está demasiado acoplada cuando depende de cómo el objeto guarda internamente sus datos, aunque ese detalle no forme parte del comportamiento público.

Ejemplo riesgoso:

def test_carrito_interno():
    carrito = Carrito()

    carrito.agregar_item("teclado")

    assert carrito._items_internos[0] == "teclado"

Si _items_internos es un detalle privado, la prueba puede romperse por un refactor que no cambia el comportamiento. Es mejor verificar mediante una propiedad o método público si existe.

12.14 Estado inicial claro

Una prueba de estado necesita un estado inicial claro. Si no entendemos cómo empieza el objeto, no podremos interpretar bien el cambio.

def test_reiniciar_contador_deja_valor_en_cero():
    contador = Contador()
    contador.incrementar()
    contador.incrementar()

    contador.reiniciar()

    assert contador.valor == 0

La preparación deja claro que el contador no estaba en cero por casualidad: primero se incrementó dos veces y luego se reinició.

12.15 Separar cambios independientes

Si una prueba verifica muchos cambios no relacionados, será difícil diagnosticar una falla. Conviene separar comportamientos independientes.

def test_pedido_confuso():
    pedido = Pedido(total=100)

    pedido.aprobar()
    pedido.agregar_nota("urgente")
    pedido.asignar_vendedor("Carlos")

    assert pedido.estado == "aprobado"
    assert pedido.nota == "urgente"
    assert pedido.vendedor == "Carlos"

Esta prueba mezcla aprobación, notas y asignación. Si son reglas independientes, deberían tener pruebas separadas.

12.16 Probar transiciones válidas

Cuando un objeto tiene estados definidos, como pendiente, aprobado o cancelado, conviene probar transiciones importantes.

class Pedido:
    def __init__(self):
        self.estado = "pendiente"

    def cancelar(self):
        if self.estado == "pendiente":
            self.estado = "cancelado"


def test_pedido_pendiente_puede_cancelarse():
    pedido = Pedido()

    pedido.cancelar()

    assert pedido.estado == "cancelado"

La prueba verifica una transición válida desde pendiente hacia cancelado.

12.17 Probar transiciones no permitidas

También conviene probar que ciertas transiciones no ocurren si la regla lo prohíbe.

def test_pedido_aprobado_no_se_cancela():
    pedido = Pedido()
    pedido.estado = "aprobado"

    pedido.cancelar()

    assert pedido.estado == "aprobado"

La prueba confirma que el método cancelar no cambia el estado cuando el pedido ya está aprobado.

12.18 Tabla de ejemplos

Unidad Estado inicial Acción Estado esperado
Cuenta Saldo 100 Depositar 50 Saldo 150
Carrito Sin ítems Agregar "teclado" Lista con "teclado"
Pedido Pendiente Aprobar Aprobado
Contador Valor 2 Reiniciar Valor 0

12.19 Lista de comprobación

Al escribir una prueba sobre cambios de estado, revisa:

  • ¿El estado inicial está claro?
  • ¿La acción principal está bien identificada?
  • ¿La aserción verifica el estado final relevante?
  • ¿La prueba evita revisar detalles internos innecesarios?
  • ¿No mezcla cambios independientes?
  • ¿Incluye casos donde el estado debe cambiar y casos donde debe mantenerse?

12.20 Qué debes recordar de este tema

  • Una prueba sobre estado verifica cómo queda un objeto después de una acción.
  • Debemos identificar estado inicial, acción y estado final esperado.
  • No todas las unidades devuelven valores; algunas modifican objetos.
  • Conviene verificar el estado observable, no detalles internos innecesarios.
  • Las transiciones válidas e inválidas suelen ser casos importantes.
  • Una prueba debe enfocarse en el cambio de estado relevante.
  • Si se mezclan muchos cambios, conviene dividir la prueba.

12.21 Conclusión

Las pruebas sobre cambios de estado son necesarias cuando una unidad modifica objetos en lugar de devolver toda la información como resultado. Son muy comunes al probar clases, métodos y entidades con comportamiento propio.

La clave está en dejar claro cómo empieza el objeto, qué acción se ejecuta y qué estado final esperamos observar.

En el próximo tema estudiaremos pruebas sobre excepciones y errores esperados, otro tipo de comportamiento importante en unidades robustas.