En este tema aplicaremos TDD a una clase pequeña. A diferencia de una función pura, una clase puede tener estado: sus métodos pueden modificar datos internos que se mantienen entre llamadas.
Trabajaremos con una CuentaBancaria para practicar creación de objetos, métodos públicos e invariantes.
El estado es la información que un objeto conserva. En una cuenta bancaria, el estado principal será el saldo.
Si depositamos dinero, el saldo cambia. Si retiramos dinero, el saldo también cambia. Las pruebas deben observar esos cambios mediante métodos o propiedades públicas.
Una invariante es una regla que siempre debe cumplirse para que el objeto sea válido.
Las pruebas nos ayudarán a proteger esa regla.
Empezaremos con una regla simple:
Primero escribimos la prueba.
Archivo a crear: tests/test_cuenta_bancaria.py
from banco.cuenta import CuentaBancaria
def test_cuenta_nueva_comienza_con_saldo_cero():
cuenta = CuentaBancaria()
assert cuenta.saldo == 0
Ejecutamos:
python -m pytest
La prueba debe fallar porque la clase todavía no existe.
Archivo a crear: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
Ejecutamos python -m pytest. La primera prueba debería pasar.
Agregamos comportamiento:
La prueba debe usar el método público que queremos diseñar.
Archivo a modificar: tests/test_cuenta_bancaria.py
from banco.cuenta import CuentaBancaria
def test_depositar_aumenta_el_saldo():
cuenta = CuentaBancaria()
cuenta.depositar(100)
assert cuenta.saldo == 100
Ejecutamos python -m pytest. La prueba debe fallar porque falta depositar.
Archivo a modificar: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, importe):
self.saldo += importe
Ejecutamos la suite. Si pasa, seguimos en verde.
Agregamos otra operación pública:
Archivo a modificar: tests/test_cuenta_bancaria.py
from banco.cuenta import CuentaBancaria
def test_retirar_disminuye_el_saldo():
cuenta = CuentaBancaria()
cuenta.depositar(100)
cuenta.retirar(40)
assert cuenta.saldo == 60
Ejecutamos python -m pytest. La prueba debe fallar porque falta retirar.
Archivo a modificar: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, importe):
self.saldo += importe
def retirar(self, importe):
self.saldo -= importe
Ejecutamos python -m pytest. Las pruebas deberían pasar.
Ahora escribimos una prueba para la regla principal: el saldo no puede quedar negativo.
Archivo a modificar: tests/test_cuenta_bancaria.py
import pytest
from banco.cuenta import CuentaBancaria
def test_no_permite_retirar_mas_que_el_saldo_disponible():
cuenta = CuentaBancaria()
cuenta.depositar(50)
with pytest.raises(ValueError):
cuenta.retirar(80)
Ejecutamos python -m pytest. La prueba debe fallar porque la implementación actual permite saldo negativo.
Archivo a modificar: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, importe):
self.saldo += importe
def retirar(self, importe):
if importe > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= importe
Ejecutamos nuevamente la suite.
python -m pytest
Además de lanzar excepción, el saldo debe quedar igual.
Archivo a modificar: tests/test_cuenta_bancaria.py
def test_retiro_rechazado_no_modifica_el_saldo():
cuenta = CuentaBancaria()
cuenta.depositar(50)
with pytest.raises(ValueError):
cuenta.retirar(80)
assert cuenta.saldo == 50
Esta prueba protege el estado después de una operación inválida.
No debería permitirse depositar cero o valores negativos.
Archivo a modificar: tests/test_cuenta_bancaria.py
@pytest.mark.parametrize("importe", [0, -10])
def test_no_permite_depositos_no_positivos(importe):
cuenta = CuentaBancaria()
with pytest.raises(ValueError):
cuenta.depositar(importe)
Ejecutamos python -m pytest. La prueba debe fallar hasta que agreguemos la validación.
Archivo a modificar: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, importe):
if importe <= 0:
raise ValueError("El depósito debe ser positivo")
self.saldo += importe
def retirar(self, importe):
if importe > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= importe
Ejecutamos la suite completa.
También deberíamos rechazar retiros de cero o valores negativos.
Archivo a modificar: tests/test_cuenta_bancaria.py
@pytest.mark.parametrize("importe", [0, -10])
def test_no_permite_retiros_no_positivos(importe):
cuenta = CuentaBancaria()
cuenta.depositar(100)
with pytest.raises(ValueError):
cuenta.retirar(importe)
Esta prueba nos guía hacia una validación similar en retirar.
Como depósito y retiro comparten una regla, podemos extraer una función privada simple.
Archivo a modificar: src/banco/cuenta.py
class CuentaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, importe):
self._validar_importe_positivo(importe)
self.saldo += importe
def retirar(self, importe):
self._validar_importe_positivo(importe)
if importe > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= importe
def _validar_importe_positivo(self, importe):
if importe <= 0:
raise ValueError("El importe debe ser positivo")
Ejecutamos python -m pytest. No probamos directamente el método privado; verificamos el comportamiento desde métodos públicos.
Agrega un método transferir_a que reciba otra cuenta y un importe. Debe retirar de la cuenta actual y depositar en la cuenta destino.
python -m pytest después de cada paso.Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio.En este tema usamos TDD para diseñar una clase pequeña con estado. Agregamos métodos públicos paso a paso, protegimos invariantes y refactorizamos validaciones compartidas sin probar detalles privados.
En el próximo tema veremos cómo un modelo de dominio puede evolucionar con pruebas primero.