Hasta ahora probamos muchas funciones que devuelven valores. Las clases agregan un aspecto importante: los objetos tienen estado interno. Un método puede modificar atributos y ese cambio debe verificarse.
En este tema probaremos una clase con saldo, movimientos y validaciones. Veremos cómo comprobar estado inicial, cambios después de llamar métodos y errores esperados.
Crea un proyecto nuevo:
mkdir pytest-clases-demo
cd pytest-clases-demo
Si pytest no está instalado en el entorno activo:
python -m pip install pytest
Crea un archivo llamado cuenta.py:
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
if saldo_inicial < 0:
raise ValueError("El saldo inicial no puede ser negativo")
self.titular = titular
self.saldo = saldo_inicial
self.movimientos = []
def depositar(self, monto):
if monto <= 0:
raise ValueError("El depósito debe ser mayor que cero")
self.saldo += monto
self.movimientos.append(("deposito", monto))
def extraer(self, monto):
if monto <= 0:
raise ValueError("La extracción debe ser mayor que cero")
if monto > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= monto
self.movimientos.append(("extraccion", monto))
def esta_activa(self):
return self.saldo > 0
def cantidad_movimientos(self):
return len(self.movimientos)
La clase modifica saldo y movimientos cuando se ejecutan sus métodos.
Crea test_cuenta.py:
import pytest
from cuenta import CuentaBancaria
def test_crear_cuenta_con_saldo_inicial():
cuenta = CuentaBancaria("Ana", 1000)
assert cuenta.titular == "Ana"
assert cuenta.saldo == 1000
assert cuenta.movimientos == []
Esta prueba verifica el estado inicial del objeto.
También conviene probar qué ocurre cuando usamos argumentos por defecto:
def test_crear_cuenta_sin_saldo_inicial_empieza_en_cero():
cuenta = CuentaBancaria("Luis")
assert cuenta.saldo == 0
assert cuenta.esta_activa() is False
La cuenta existe, pero no está activa porque su saldo es cero.
El método depositar modifica el saldo y agrega un movimiento:
def test_depositar_incrementa_saldo():
cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500)
assert cuenta.saldo == 1500
assert cuenta.movimientos == [("deposito", 500)]
Después de llamar al método, verificamos cómo quedó el objeto.
El método extraer reduce el saldo y registra la extracción:
def test_extraer_reduce_saldo():
cuenta = CuentaBancaria("Ana", 1000)
cuenta.extraer(300)
assert cuenta.saldo == 700
assert cuenta.movimientos == [("extraccion", 300)]
Como varias pruebas necesitan una cuenta con saldo, podemos crear una fixture:
@pytest.fixture
def cuenta():
return CuentaBancaria("Ana", 1000)
Ahora las pruebas pueden pedir cuenta como argumento.
La prueba de depósito queda más breve:
def test_depositar_incrementa_saldo(cuenta):
cuenta.depositar(500)
assert cuenta.saldo == 1500
assert cuenta.movimientos == [("deposito", 500)]
Cada prueba recibe una cuenta nueva porque el alcance por defecto de la fixture es function.
Podemos ejecutar una secuencia de operaciones y verificar el estado final:
def test_deposito_y_extraccion_actualizan_saldo_y_movimientos(cuenta):
cuenta.depositar(500)
cuenta.extraer(200)
assert cuenta.saldo == 1300
assert cuenta.movimientos == [
("deposito", 500),
("extraccion", 200),
]
Esta prueba verifica una interacción más completa entre métodos.
Los métodos que no modifican estado también deben probarse:
def test_cuenta_con_saldo_esta_activa(cuenta):
assert cuenta.esta_activa() is True
def test_cantidad_movimientos(cuenta):
cuenta.depositar(100)
cuenta.extraer(50)
assert cuenta.cantidad_movimientos() == 2
El constructor también puede validar datos:
def test_saldo_inicial_negativo_lanza_error():
with pytest.raises(ValueError):
CuentaBancaria("Ana", -100)
Los métodos con validaciones deben probar sus errores esperados:
def test_deposito_cero_lanza_error(cuenta):
with pytest.raises(ValueError):
cuenta.depositar(0)
def test_extraccion_mayor_al_saldo_lanza_error(cuenta):
with pytest.raises(ValueError):
cuenta.extraer(2000)
Cuando un método falla, muchas veces esperamos que el objeto quede igual:
def test_extraccion_invalida_no_modifica_saldo(cuenta):
with pytest.raises(ValueError):
cuenta.extraer(2000)
assert cuenta.saldo == 1000
assert cuenta.movimientos == []
Esta prueba protege una regla importante: una operación rechazada no debe alterar la cuenta.
El archivo test_cuenta.py puede quedar así:
import pytest
from cuenta import CuentaBancaria
@pytest.fixture
def cuenta():
return CuentaBancaria("Ana", 1000)
def test_crear_cuenta_con_saldo_inicial():
cuenta = CuentaBancaria("Ana", 1000)
assert cuenta.titular == "Ana"
assert cuenta.saldo == 1000
assert cuenta.movimientos == []
def test_crear_cuenta_sin_saldo_inicial_empieza_en_cero():
cuenta = CuentaBancaria("Luis")
assert cuenta.saldo == 0
assert cuenta.esta_activa() is False
def test_depositar_incrementa_saldo(cuenta):
cuenta.depositar(500)
assert cuenta.saldo == 1500
assert cuenta.movimientos == [("deposito", 500)]
def test_extraer_reduce_saldo(cuenta):
cuenta.extraer(300)
assert cuenta.saldo == 700
assert cuenta.movimientos == [("extraccion", 300)]
def test_deposito_y_extraccion_actualizan_saldo_y_movimientos(cuenta):
cuenta.depositar(500)
cuenta.extraer(200)
assert cuenta.saldo == 1300
assert cuenta.movimientos == [
("deposito", 500),
("extraccion", 200),
]
def test_cuenta_con_saldo_esta_activa(cuenta):
assert cuenta.esta_activa() is True
def test_cantidad_movimientos(cuenta):
cuenta.depositar(100)
cuenta.extraer(50)
assert cuenta.cantidad_movimientos() == 2
def test_saldo_inicial_negativo_lanza_error():
with pytest.raises(ValueError):
CuentaBancaria("Ana", -100)
def test_deposito_cero_lanza_error(cuenta):
with pytest.raises(ValueError):
cuenta.depositar(0)
def test_extraccion_mayor_al_saldo_lanza_error(cuenta):
with pytest.raises(ValueError):
cuenta.extraer(2000)
def test_extraccion_invalida_no_modifica_saldo(cuenta):
with pytest.raises(ValueError):
cuenta.extraer(2000)
assert cuenta.saldo == 1000
assert cuenta.movimientos == []
Ejecuta:
python -m pytest
La salida esperada será similar a:
collected 11 items
test_cuenta.py ........... [100%]
11 passed in 0.03s
Para ver cada prueba:
python -m pytest -v
La salida detallada ayuda a revisar qué comportamiento cubre cada caso.
| Aspecto | Ejemplo |
|---|---|
| Estado inicial | saldo == 1000, movimientos == [] |
| Cambio después de un método | depositar incrementa el saldo. |
| Registro de operaciones | movimientos guarda depósitos y extracciones. |
| Errores sin efectos secundarios | Una extracción inválida no cambia el saldo. |
mkdir pytest-clases-demo
cd pytest-clases-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_cuenta.py::test_extraccion_invalida_no_modifica_saldo -v
En este tema probamos una clase con estado interno. Verificamos construcción, métodos que modifican atributos, métodos de consulta y errores esperados.
En el próximo tema trabajaremos con validaciones, casos borde y datos inválidos, reforzando cómo elegir entradas importantes para una suite de pruebas.