En una aplicación orientada a objetos, muchas unidades de código son clases y métodos. Probar una clase no significa revisar cada línea interna, sino verificar los comportamientos que la clase ofrece a través de sus métodos públicos.
Este tema muestra cómo pensar las pruebas cuando una unidad tiene estado, métodos que lo modifican y métodos que calculan resultados a partir de ese estado.
Al probar una clase, la unidad puede ser la clase completa o un método específico dentro de ella. La decisión depende de la responsabilidad que queramos verificar.
En general, conviene probar la clase desde su interfaz pública: crear un objeto, llamar sus métodos y verificar el resultado observable.
El caso más simple aparece cuando un método calcula y devuelve un valor sin modificar el estado del objeto.
class Calculadora:
def sumar(self, a, b):
return a + b
def test_sumar_devuelve_la_suma_de_dos_numeros():
calculadora = Calculadora()
resultado = calculadora.sumar(4, 6)
assert resultado == 10
La prueba crea el objeto, ejecuta el método y verifica el valor devuelto.
Una clase suele guardar datos en atributos. Cuando un método usa ese estado para calcular una respuesta, la prueba debe preparar el objeto en un estado conocido.
class Cuenta:
def __init__(self, saldo):
self.saldo = saldo
def saldo_disponible(self):
return self.saldo
def test_saldo_disponible_devuelve_el_saldo_actual():
cuenta = Cuenta(1500)
assert cuenta.saldo_disponible() == 1500
Cuando un método modifica el estado del objeto, la prueba debe verificar el cambio observable después de ejecutarlo.
class Contador:
def __init__(self):
self.valor = 0
def incrementar(self):
self.valor += 1
def obtener_valor(self):
return self.valor
def test_incrementar_aumenta_el_valor_en_uno():
contador = Contador()
contador.incrementar()
assert contador.obtener_valor() == 1
La prueba no necesita conocer cada detalle interno, solo el efecto que el método produce.
Si una clase ofrece métodos públicos para consultar su estado, es preferible usarlos en la prueba en lugar de acceder directamente a atributos internos.
class Carrito:
def __init__(self):
self._productos = []
def agregar(self, producto):
self._productos.append(producto)
def cantidad(self):
return len(self._productos)
def test_agregar_producto_incrementa_la_cantidad():
carrito = Carrito()
carrito.agregar("mouse")
assert carrito.cantidad() == 1
Si más adelante la clase cambia la forma de guardar productos, la prueba puede seguir siendo válida mientras el comportamiento público no cambie.
Algunas clases solo muestran su comportamiento después de una secuencia de llamadas. Esa secuencia debe ser breve y clara.
class Pila:
def __init__(self):
self._items = []
def apilar(self, item):
self._items.append(item)
def desapilar(self):
return self._items.pop()
def test_desapilar_devuelve_el_ultimo_elemento_apilado():
pila = Pila()
pila.apilar("primero")
pila.apilar("segundo")
resultado = pila.desapilar()
assert resultado == "segundo"
El estado inicial de un objeto puede ser parte importante de su contrato. Si otros métodos dependen de ese estado, conviene probarlo.
class Pedido:
def __init__(self):
self._confirmado = False
def esta_confirmado(self):
return self._confirmado
def test_pedido_inicia_sin_confirmar():
pedido = Pedido()
assert pedido.esta_confirmado() is False
Una transición ocurre cuando un objeto pasa de un estado a otro. Las pruebas deben mostrar qué acción provoca el cambio.
class Pedido:
def __init__(self):
self._confirmado = False
def confirmar(self):
self._confirmado = True
def esta_confirmado(self):
return self._confirmado
def test_confirmar_cambia_el_estado_del_pedido():
pedido = Pedido()
pedido.confirmar()
assert pedido.esta_confirmado() is True
Cuando un método contiene una condición, la prueba debe cubrir los ejemplos relevantes de esa regla.
class Cliente:
def __init__(self, puntos):
self.puntos = puntos
def es_vip(self):
return self.puntos >= 1000
def test_cliente_con_mil_puntos_es_vip():
cliente = Cliente(1000)
assert cliente.es_vip() is True
El valor 1000 es importante porque está justo en el límite de la condición.
Cuando un método agrega información a una colección interna, se debe verificar el resultado observable de esa operación.
class ListaDeTareas:
def __init__(self):
self._tareas = []
def agregar(self, descripcion):
self._tareas.append(descripcion)
def cantidad(self):
return len(self._tareas)
def test_agregar_tarea_incrementa_la_cantidad():
lista = ListaDeTareas()
lista.agregar("Estudiar pruebas unitarias")
assert lista.cantidad() == 1
Las operaciones de eliminación pueden tener errores por índices, búsquedas incorrectas o estados vacíos. Por eso conviene probar el efecto visible.
class ListaDeTareas:
def __init__(self):
self._tareas = []
def agregar(self, descripcion):
self._tareas.append(descripcion)
def eliminar(self, descripcion):
self._tareas.remove(descripcion)
def contiene(self, descripcion):
return descripcion in self._tareas
def test_eliminar_tarea_la_quita_de_la_lista():
lista = ListaDeTareas()
lista.agregar("Comprar pan")
lista.eliminar("Comprar pan")
assert lista.contiene("Comprar pan") is False
Un método puede rechazar una operación inválida. Esa situación también forma parte del comportamiento de la clase.
class Cuenta:
def __init__(self, saldo):
self.saldo = saldo
def retirar(self, importe):
if importe > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= importe
def test_retirar_mas_que_el_saldo_genera_error():
cuenta = Cuenta(500)
try:
cuenta.retirar(800)
assert False
except ValueError as error:
assert str(error) == "Saldo insuficiente"
Además de verificar el error, a veces importa confirmar que el objeto queda igual después de una operación rechazada.
class Cuenta:
def __init__(self, saldo):
self.saldo = saldo
def retirar(self, importe):
if importe > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= importe
def test_retiro_rechazado_no_cambia_el_saldo():
cuenta = Cuenta(500)
try:
cuenta.retirar(800)
except ValueError:
pass
assert cuenta.saldo == 500
Una clase puede delegar parte de su trabajo en una dependencia. Para mantener la prueba unitaria, podemos usar un objeto simple que reemplace esa dependencia.
class Facturador:
def __init__(self, repositorio):
self.repositorio = repositorio
def registrar_factura(self, numero):
self.repositorio.guardar(numero)
class RepositorioFalso:
def __init__(self):
self.guardados = []
def guardar(self, numero):
self.guardados.append(numero)
def test_registrar_factura_guarda_el_numero():
repositorio = RepositorioFalso()
facturador = Facturador(repositorio)
facturador.registrar_factura("A-0001")
assert repositorio.guardados == ["A-0001"]
Una prueba debe nombrar el comportamiento, no el detalle de implementación. Si el método ordena internamente, filtra o usa una estructura auxiliar, la prueba debe enfocarse en el resultado que recibe quien usa la clase.
class Catalogo:
def __init__(self, productos):
self.productos = productos
def disponibles(self):
return [producto for producto in self.productos if producto["stock"] > 0]
def test_disponibles_devuelve_solo_productos_con_stock():
catalogo = Catalogo([
{"nombre": "teclado", "stock": 3},
{"nombre": "monitor", "stock": 0}
])
resultado = catalogo.disponibles()
assert resultado == [{"nombre": "teclado", "stock": 3}]
Una prueba está demasiado acoplada cuando se rompe ante cualquier refactorización interna, aunque la clase siga funcionando igual.
Para evitarlo, prueba desde los métodos públicos, usa datos simples y evita verificar variables temporales, atributos privados o pasos internos que no forman parte del contrato de la clase.
La preparación del objeto debe ser fácil de leer. Si una prueba necesita muchos pasos para llegar al estado requerido, conviene revisar si la clase tiene demasiadas responsabilidades o si la prueba está intentando cubrir demasiado.
def test_carrito_vacio_tiene_total_cero():
carrito = Carrito()
assert carrito.cantidad() == 0
Una preparación breve ayuda a que la intención de la prueba se entienda rápidamente.
Esta tabla resume qué observar al probar clases y métodos.
| Situación | Qué verificar | Ejemplo |
|---|---|---|
| Método que devuelve valor | Resultado devuelto | sumar(4, 6) == 10 |
| Método que cambia estado | Estado observable posterior | contador.obtener_valor() == 1 |
| Método que rechaza operación | Error esperado | ValueError |
| Método que usa dependencia | Interacción relevante | Repositorio falso recibió el dato |
Al probar clases y métodos, ten presentes estas ideas:
Las pruebas unitarias de clases y métodos se enfocan en el comportamiento observable de los objetos. Una clase puede devolver valores, cambiar estado, rechazar operaciones o colaborar con otra dependencia, y cada caso requiere una verificación clara.
La clave es no probar la clase desde sus detalles internos, sino desde lo que promete a quien la usa. Así las pruebas acompañan la evolución del código sin romperse ante cada cambio de implementación.