Las reglas de negocio expresan decisiones propias del dominio de una aplicación. No son simples detalles técnicos: representan cómo debe comportarse el sistema según las políticas, restricciones y acuerdos del negocio.
Por ejemplo, una tienda puede definir cuándo un pedido obtiene envío gratis, un banco puede decidir cuándo una cuenta permite retirar dinero, una plataforma puede establecer qué usuarios acceden a una promoción y una escuela puede calcular si un estudiante aprueba una materia.
Probar estas reglas de manera unitaria es especialmente valioso porque un error pequeño puede producir consecuencias importantes: descuentos incorrectos, pedidos aprobados indebidamente, usuarios rechazados sin motivo o cálculos que afectan reportes y decisiones reales.
Una regla de negocio es una condición, cálculo o decisión que surge del funcionamiento esperado del dominio, no de una necesidad técnica interna.
Algunos ejemplos son:
Estas reglas suelen cambiar con el tiempo. Por eso conviene expresarlas en código claro y acompañarlas con pruebas que documenten ejemplos concretos.
Una validación comprueba si un dato tiene una forma aceptable: no vacío, dentro de un rango, con formato correcto o con valores permitidos. Una regla de negocio, en cambio, decide qué debe ocurrir en una situación del dominio.
La frontera no siempre es perfecta, pero la distinción ayuda a diseñar pruebas:
| Tipo | Pregunta que responde | Ejemplo |
|---|---|---|
| Validación | ¿El dato tiene una forma aceptable? | La edad no puede ser negativa. |
| Regla de negocio | ¿Qué decisión corresponde según el dominio? | Una persona de 18 años puede registrarse. |
En las pruebas unitarias, ambas pueden tener casos límite, pero las reglas de negocio suelen requerir más atención a combinaciones de condiciones y consecuencias esperadas.
Empecemos con una regla sencilla: un cliente obtiene envío gratis si el total de su compra es mayor o igual a 10000.
def tiene_envio_gratis(total):
return total >= 10000
def test_compra_de_10000_tiene_envio_gratis():
assert tiene_envio_gratis(10000) is True
def test_compra_de_9999_no_tiene_envio_gratis():
assert tiene_envio_gratis(9999) is False
Estas pruebas protegen el límite de la regla. Si alguien cambia la comparación a total > 10000, la primera prueba falla y muestra que el importe exacto dejó de aceptarse.
Las reglas de negocio deben leerse con claridad en los nombres de las pruebas. Un buen nombre evita que el lector tenga que interpretar datos aislados sin contexto.
def es_cliente_vip(puntos):
return puntos >= 1000
def test_cliente_con_1000_puntos_es_vip():
assert es_cliente_vip(1000) is True
def test_cliente_con_999_puntos_no_es_vip():
assert es_cliente_vip(999) is False
Los nombres explican la regla con ejemplos. Esto convierte a las pruebas en documentación ejecutable: muestran qué significa ser cliente VIP en términos concretos.
Muchas reglas de negocio calculan importes, comisiones, recargos, descuentos o puntajes. En estos casos, la prueba debe verificar el valor final esperado con datos simples.
def calcular_descuento(total, es_vip):
if es_vip:
return total * 0.15
return total * 0.05
def test_cliente_vip_recibe_descuento_del_15_por_ciento():
assert calcular_descuento(1000, True) == 150
def test_cliente_no_vip_recibe_descuento_del_5_por_ciento():
assert calcular_descuento(1000, False) == 50
Usar 1000 facilita leer el resultado esperado. Si elegimos datos innecesariamente difíciles, la prueba se vuelve más costosa de entender.
Una regla puede tener varios caminos posibles. Cada camino relevante debería tener al menos una prueba que lo represente.
def categoria_cliente(compras_anuales):
if compras_anuales >= 50:
return "oro"
if compras_anuales >= 20:
return "plata"
return "bronce"
def test_cliente_con_50_compras_es_oro():
assert categoria_cliente(50) == "oro"
def test_cliente_con_20_compras_es_plata():
assert categoria_cliente(20) == "plata"
def test_cliente_con_5_compras_es_bronce():
assert categoria_cliente(5) == "bronce"
Estas pruebas cubren las tres respuestas posibles. Los valores 50 y 20 son límites importantes, no números elegidos al azar.
Las reglas de negocio suelen combinar varias condiciones. El riesgo es probar solo el caso feliz y olvidar qué ocurre cuando una condición no se cumple.
def puede_aprobar_prestamo(ingresos, deuda_activa, antiguedad_meses):
return ingresos >= 300000 and deuda_activa is False and antiguedad_meses >= 12
def test_solicitante_con_ingresos_sin_deuda_y_antiguedad_aprueba():
assert puede_aprobar_prestamo(350000, False, 18) is True
def test_solicitante_con_deuda_activa_no_aprueba():
assert puede_aprobar_prestamo(350000, True, 18) is False
def test_solicitante_sin_antiguedad_suficiente_no_aprueba():
assert puede_aprobar_prestamo(350000, False, 6) is False
Cada prueba modifica una condición relevante. Así se puede identificar qué parte de la regla protege cada caso.
A veces varias reglas podrían aplicar al mismo tiempo, pero una tiene prioridad sobre otra. Ese comportamiento debe probarse explícitamente porque suele generar errores difíciles de ver.
def calcular_comision(total, cliente_vip, producto_en_promocion):
if producto_en_promocion:
return 0
if cliente_vip:
return total * 0.02
return total * 0.05
def test_producto_en_promocion_no_genera_comision_aunque_cliente_sea_vip():
comision = calcular_comision(1000, True, True)
assert comision == 0
La prueba documenta una prioridad: la promoción anula la comisión incluso si el cliente es VIP. Sin un caso así, alguien podría reordenar las condiciones y cambiar la política sin advertirlo.
Muchas reglas no solo calculan resultados, también rechazan una operación. En esos casos debemos comprobar que el rechazo ocurra en la situación correcta.
def puede_venderse(producto, cantidad):
return producto["stock"] >= cantidad and producto["activo"] is True
def test_producto_activo_con_stock_suficiente_puede_venderse():
producto = {"stock": 5, "activo": True}
assert puede_venderse(producto, 3) is True
def test_producto_sin_stock_suficiente_no_puede_venderse():
producto = {"stock": 2, "activo": True}
assert puede_venderse(producto, 3) is False
def test_producto_inactivo_no_puede_venderse_aunque_tenga_stock():
producto = {"stock": 5, "activo": False}
assert puede_venderse(producto, 3) is False
El último caso es importante porque muestra que el stock no alcanza si el producto está inactivo. La prueba protege una decisión específica del negocio.
Algunas reglas de negocio modifican el estado de un objeto. La prueba debe verificar el efecto observable después de ejecutar la operación.
class Pedido:
def __init__(self, total):
self.total = total
self.estado = "pendiente"
def confirmar(self):
if self.total <= 0:
raise ValueError("El pedido no tiene total valido")
self.estado = "confirmado"
def test_confirmar_pedido_con_total_valido_cambia_estado():
pedido = Pedido(1000)
pedido.confirmar()
assert pedido.estado == "confirmado"
La prueba no necesita verificar cómo se guarda internamente el estado si la clase ofrece un método público para consultarlo. Lo importante es observar el cambio que la regla promete.
Cuando una operación de negocio se rechaza, también puede ser importante comprobar que el objeto queda en un estado consistente.
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_modifica_saldo():
cuenta = Cuenta(500)
try:
cuenta.retirar(800)
except ValueError:
pass
assert cuenta.saldo == 500
Esta prueba protege una consecuencia importante: la cuenta no debe quedar parcialmente modificada después de una operación inválida.
Las reglas basadas en fechas pueden volverse frágiles si dependen directamente de la fecha actual del sistema. Para probarlas de manera unitaria, conviene pasar la fecha como dato de entrada.
from datetime import date
def puede_cancelar_sin_costo(fecha_reserva, fecha_cancelacion):
dias_restantes = (fecha_reserva - fecha_cancelacion).days
return dias_restantes >= 1
def test_reserva_cancelada_un_dia_antes_no_tiene_costo():
resultado = puede_cancelar_sin_costo(date(2026, 5, 10), date(2026, 5, 9))
assert resultado is True
def test_reserva_cancelada_el_mismo_dia_tiene_costo():
resultado = puede_cancelar_sin_costo(date(2026, 5, 10), date(2026, 5, 10))
assert resultado is False
Al pasar las fechas explícitamente, las pruebas son determinísticas: no cambian su resultado según el día en que se ejecutan.
Los cálculos monetarios, porcentajes y comisiones pueden incluir redondeos. Si el redondeo es parte de la regla, debe probarse con ejemplos específicos.
def calcular_recargo(importe):
return round(importe * 0.03, 2)
def test_recargo_se_redondea_a_dos_decimales():
assert calcular_recargo(333.33) == 10.0
Este tipo de prueba debe ser claro sobre la política esperada. En sistemas reales, para dinero suele preferirse usar tipos decimales en lugar de números de punto flotante, pero la idea de la prueba se mantiene: verificar el resultado de negocio, incluido el redondeo.
Para probar una regla no siempre necesitamos construir objetos completos de la aplicación. Muchas veces alcanza con datos simples que representen lo necesario para la decisión.
def calcular_puntos_de_compra(compra):
if compra["categoria"] == "premium":
return compra["total"] // 10
return compra["total"] // 20
def test_compra_premium_acumula_mas_puntos():
compra = {"categoria": "premium", "total": 1000}
assert calcular_puntos_de_compra(compra) == 100
def test_compra_normal_acumula_menos_puntos():
compra = {"categoria": "normal", "total": 1000}
assert calcular_puntos_de_compra(compra) == 50
La prueba usa solo los campos que la regla necesita. Esto evita preparar datos irrelevantes que distraen del comportamiento principal.
Una regla de negocio es más fácil de probar cuando está separada de bases de datos, APIs, archivos o envío de mensajes. La consulta o persistencia puede quedar fuera de la unidad probada.
Por ejemplo, calcular si un pedido obtiene descuento puede ser una función pura. Guardar ese pedido en una base de datos es otra responsabilidad.
Esta separación permite que las pruebas de negocio sean rápidas y determinísticas, mientras las integraciones se prueban con otro tipo de pruebas.
Algunas reglas necesitan consultar información de una dependencia. Si queremos mantener la prueba unitaria, podemos reemplazar esa dependencia por un objeto falso simple.
class RepositorioClientesFalso:
def __init__(self, puntos):
self.puntos = puntos
def obtener_puntos(self, cliente_id):
return self.puntos
class ServicioBeneficios:
def __init__(self, repositorio_clientes):
self.repositorio_clientes = repositorio_clientes
def puede_acceder_a_beneficio(self, cliente_id):
puntos = self.repositorio_clientes.obtener_puntos(cliente_id)
return puntos >= 1000
def test_cliente_con_1000_puntos_accede_a_beneficio():
repositorio = RepositorioClientesFalso(1000)
servicio = ServicioBeneficios(repositorio)
assert servicio.puede_acceder_a_beneficio(15) is True
El objeto falso evita depender de una base de datos real. La prueba se concentra en la regla: 1000 puntos permiten acceder al beneficio.
Si una regla de negocio se prueba solo desde una pantalla o un flujo completo, el diagnóstico se vuelve más difícil. Un fallo podría estar en la interfaz, en la conversión de datos, en la base de datos, en permisos o en la regla misma.
Las pruebas unitarias permiten verificar la decisión directamente. Luego puede haber pruebas de integración o end-to-end para comprobar que esa regla se conecta bien con el resto del sistema.
La idea no es evitar pruebas de mayor nivel, sino no usarlas como único mecanismo para proteger decisiones importantes del dominio.
Una mala prueba calcula el resultado esperado repitiendo la misma lógica que el código probado. Eso reduce la capacidad de detectar errores.
def aplica_descuento(total, antiguedad_anios):
return total >= 5000 and antiguedad_anios >= 2
def test_descuento_ejemplo_poco_util():
total = 6000
antiguedad_anios = 3
assert aplica_descuento(total, antiguedad_anios) == (total >= 5000 and antiguedad_anios >= 2)
Una prueba más útil expresa casos del negocio:
def test_cliente_con_compra_minima_y_antiguedad_suficiente_aplica_descuento():
assert aplica_descuento(5000, 2) is True
def test_cliente_sin_antiguedad_suficiente_no_aplica_descuento():
assert aplica_descuento(5000, 1) is False
La prueba debe representar ejemplos esperados, no reconstruir el algoritmo.
Cuando una regla tiene varios escenarios, conviene organizar las pruebas alrededor de esos escenarios. Eso puede hacerse con nombres claros, agrupación por archivo o clases de prueba si el framework lo permite.
Una buena organización permite responder rápidamente:
Si una prueba intenta responder todas estas preguntas a la vez, probablemente deba dividirse.
Esta tabla resume situaciones comunes al probar reglas de negocio.
| Situación | Qué probar | Ejemplo |
|---|---|---|
| Decisión simple | Caso aceptado, caso rechazado y límite. | Envío gratis desde 10000. |
| Cálculo | Resultado final esperado con datos simples. | Descuento del 15% para cliente VIP. |
| Regla compuesta | Un caso válido y un caso por cada condición que rechaza. | Préstamo con ingresos, deuda y antigüedad. |
| Prioridad entre reglas | Qué regla gana cuando varias aplican. | Promoción anula comisión. |
| Cambio de estado | Estado observable después de la operación. | Pedido pasa de pendiente a confirmado. |
Al probar reglas de negocio, conviene evitar estos errores:
Una suite útil debe funcionar como memoria del comportamiento esperado del dominio, no como una acumulación de casos difíciles de interpretar.
Las reglas de negocio son una de las partes más importantes de una aplicación. Allí se expresan decisiones que afectan operaciones reales: aprobar, rechazar, calcular, clasificar, cobrar, descontar o cambiar estados.
Probarlas unitariamente permite verificar esas decisiones con ejemplos pequeños, rápidos y repetibles. Una prueba bien escrita muestra qué política se espera, qué casos se aceptan, qué casos se rechazan y qué ocurre en los límites.
En el próximo tema veremos las pruebas parametrizadas, una técnica especialmente útil cuando una misma regla debe comprobarse con muchos valores similares.