Después de entender qué son las pruebas unitarias, sus objetivos, sus beneficios y sus límites, aparece una pregunta práctica: ¿cuándo conviene escribirlas?
La respuesta no debería ser "siempre, para todo" ni "solo al final si queda tiempo". Las pruebas unitarias aportan más valor cuando se escriben en momentos adecuados y sobre comportamientos importantes.
En este tema veremos situaciones concretas en las que conviene escribir pruebas unitarias, momentos del desarrollo donde resultan especialmente útiles y casos donde quizá sea mejor elegir otro tipo de prueba.
Una de las mejores situaciones para escribir pruebas unitarias es cuando existe una regla de negocio concreta. Las reglas de negocio suelen contener decisiones, límites, excepciones o cálculos importantes.
Ejemplos:
Estas reglas merecen pruebas porque un error puede afectar directamente al comportamiento esperado del sistema.
Supongamos que una compra obtiene envío gratis cuando el total es igual o superior a 5000.
def tiene_envio_gratis(total):
return total >= 5000
def test_total_menor_a_5000_no_tiene_envio_gratis():
assert tiene_envio_gratis(4999) == False
def test_total_igual_a_5000_tiene_envio_gratis():
assert tiene_envio_gratis(5000) == True
Este es un buen momento para escribir pruebas unitarias porque el límite es importante. Un pequeño error, como usar > en lugar de >=, cambiaría la regla.
Una prueba unitaria puede escribirse antes de implementar el código. Esta práctica es parte del enfoque TDD, que estudiaremos en un curso específico, pero la idea básica es simple: primero expresamos el comportamiento esperado y luego escribimos el código que lo cumple.
Esto conviene cuando la regla está clara y podemos describirla mediante ejemplos concretos.
def test_descuento_para_cliente_vip():
assert calcular_descuento(tipo_cliente="vip", monto=1000) == 150
Aunque la función calcular_descuento todavía no exista, la prueba expresa una expectativa: un cliente VIP obtiene 150 de descuento sobre una compra de 1000. Luego implementaremos el código necesario.
También conviene escribir pruebas unitarias mientras se desarrolla una funcionalidad. A medida que aparece una nueva regla o caso relevante, podemos agregar una prueba que la verifique.
Este enfoque es útil cuando aún estamos explorando la solución, pero ya conocemos algunos comportamientos importantes. La prueba ayuda a confirmar que cada pieza funciona antes de conectarla con el resto del sistema.
Por ejemplo, antes de integrar una regla de impuestos en una pantalla de facturación, podemos probar la función de cálculo directamente.
Una de las mejores oportunidades para agregar una prueba unitaria aparece cuando encontramos un defecto. Si el defecto se originó en una unidad de código, conviene escribir una prueba que lo reproduzca.
El proceso puede ser:
Esta práctica convierte un defecto encontrado en una protección permanente contra regresiones.
Supongamos que una función que calcula promedios falla con listas vacías. Al encontrar el defecto, podemos escribir una prueba que exprese el comportamiento esperado.
def promedio(numeros):
if len(numeros) == 0:
return 0
return sum(numeros) / len(numeros)
def test_promedio_de_lista_vacia_es_cero():
assert promedio([]) == 0
La prueba documenta una decisión: para una lista vacía, el promedio esperado es 0. Si en el futuro alguien modifica la función y rompe ese caso, la prueba lo señalará.
Antes de refactorizar código existente, conviene tener pruebas unitarias para los comportamientos importantes. Refactorizar sin pruebas puede ser riesgoso, porque podríamos cambiar accidentalmente lo que el código hace.
Si el código no tiene pruebas, una estrategia práctica es escribir algunas pruebas de caracterización. Estas pruebas registran el comportamiento actual antes de modificar la estructura interna.
No siempre necesitamos cubrir todo antes de refactorizar, pero sí conviene proteger las reglas más importantes y los casos que podrían romperse.
Cuando debemos cambiar una funcionalidad existente, las pruebas unitarias ayudan a responder dos preguntas:
Si agregamos una regla nueva, conviene escribir una prueba para esa regla. Si tocamos código con reglas antiguas, conviene ejecutar o agregar pruebas que protejan los casos que no deberían cambiar.
Los casos límite son excelentes candidatos para pruebas unitarias. Un caso límite aparece en el borde de una regla: justo antes, justo en el límite y justo después.
Ejemplos:
Estos valores suelen revelar errores en condiciones como >, >=, < o <=.
Supongamos que una contraseña debe tener al menos 8 caracteres.
def password_tiene_longitud_valida(password):
return len(password) >= 8
def test_password_de_7_caracteres_no_es_valido():
assert password_tiene_longitud_valida("abcdefg") == False
def test_password_de_8_caracteres_es_valido():
assert password_tiene_longitud_valida("abcdefgh") == True
Este tipo de prueba es simple, rápida y útil. Protege una regla que podría romperse fácilmente por un cambio pequeño.
Una unidad con condicionales suele necesitar pruebas para sus caminos principales. No se trata de probar combinaciones sin criterio, sino de cubrir decisiones relevantes.
def clasificar_temperatura(grados):
if grados < 10:
return "frio"
if grados <= 25:
return "templado"
return "calor"
def test_temperatura_baja_es_frio():
assert clasificar_temperatura(5) == "frio"
def test_temperatura_intermedia_es_templado():
assert clasificar_temperatura(20) == "templado"
def test_temperatura_alta_es_calor():
assert clasificar_temperatura(30) == "calor"
Cuando una función toma decisiones, cada rama importante debería tener al menos un caso representativo.
Si una regla puede generar dudas, una prueba unitaria ayuda a dejarla explícita. Esto ocurre con reglas de negocio, cálculos financieros, redondeos, prioridades o excepciones.
Por ejemplo, si un sistema redondea importes hacia arriba en algunos casos y hacia abajo en otros, conviene escribir pruebas que documenten esas decisiones.
Cuando alguien lea la prueba, no tendrá que adivinar la intención del código. Verá ejemplos concretos del comportamiento esperado.
Una unidad crítica es aquella cuyo error puede tener impacto importante. Por ejemplo:
Cuanto mayor es el impacto de una falla, más sentido tiene proteger la unidad con pruebas claras.
El código que cambia con frecuencia tiene más oportunidades de romperse. Si una unidad es modificada muchas veces, las pruebas unitarias ayudan a detectar regresiones.
Esto suele ocurrir en reglas de negocio activas, cálculos que se ajustan por nuevas condiciones, validadores que cambian por requisitos nuevos o transformadores de datos que deben adaptarse a formatos distintos.
En estas zonas del código, una prueba bien elegida puede ahorrar muchas revisiones manuales.
Algunos errores no son fáciles de descubrir desde la interfaz. Pueden aparecer solo con ciertos datos, combinaciones o casos límite. Una prueba unitaria permite apuntar directamente a esos casos.
Ejemplo: una regla de comisión puede fallar solo cuando el importe es exactamente cero, o una conversión puede fallar solo con texto vacío. Probar esos casos manualmente en cada versión sería lento y fácil de olvidar.
Si un caso es importante pero molesto de verificar manualmente, suele ser buen candidato para automatización unitaria.
No todo comportamiento necesita una prueba unitaria aislada. Puede no convenir cuando:
La decisión correcta no siempre es "más pruebas unitarias". Escribir pruebas útiles implica elegir el nivel adecuado para el riesgo que queremos cubrir.
| Situación | ¿Conviene prueba unitaria? | Motivo |
|---|---|---|
| Regla de negocio con límites claros. | Sí | Se puede verificar con casos concretos. |
| Cálculo de importes. | Sí | Un error puede tener impacto directo. |
| Función que solo llama a otra función sin lógica propia. | No siempre | Puede no aportar información adicional. |
| Servicio que guarda en base de datos. | Depende | La lógica puede probarse unitariamente, pero la persistencia requiere integración. |
| Flujo completo de registro desde la interfaz. | No | Corresponde a prueba end-to-end o de interfaz. |
| Defecto encontrado en una función específica. | Sí | La prueba evita que el defecto vuelva. |
Cuando dudes si escribir una prueba unitaria, puedes seguir este razonamiento:
Este orden evita escribir pruebas por impulso y ayuda a elegir casos con intención.
Las pruebas unitarias convienen cuando permiten verificar un comportamiento importante de forma rápida, clara y aislada. Son especialmente útiles para reglas de negocio, cálculos, validaciones, defectos corregidos, casos límite y código que cambia con frecuencia.
También debemos reconocer cuándo no son la herramienta adecuada. Si el riesgo principal está en la colaboración entre componentes o en un flujo completo de usuario, probablemente necesitamos otro nivel de prueba.
En el próximo tema estudiaremos la anatomía de una prueba: preparación, ejecución y verificación.