Atlasingeniería

Algoritmos y programaciónProgramación orientada a objetosTema 4

Pruebas automatizadas y depuración

Una prueba ejecuta un comportamiento y verifica una propiedad repetible. Depurar usa evidencia para reducir hipótesis hasta encontrar la causa, no sólo el síntoma.

Para este tema conviene tener claro:Funciones, parámetros y alcance

Una prueba no demuestra que el programa no tiene errores. Demuestra que cierto comportamiento, bajo condiciones explícitas, produjo el resultado esperado. Su valor está en repetir esa evidencia cada vez que el código cambia.

Preparar, ejecutar y afirmar

Una prueba clara prepara datos, ejecuta una acción y afirma resultados. El nombre debería contar el escenario y la garantía, no repetir el nombre del método.

Los casos incluyen una entrada representativa, fronteras, vacío y errores esperados. Copiar muchos ejemplos similares aumenta volumen sin necesariamente cubrir otra clase de comportamiento.

Antes de seguir, predecí

Un test comprueba que la función devuelve un número. ¿Qué detecta?

Unitaria, de integración y de punta a punta

Una prueba unitaria aísla una regla pequeña y da diagnóstico rápido. Una de integración verifica que componentes reales se entiendan: serialización, base de datos o red. Una prueba de extremo a extremo recorre un flujo visible y ofrece más confianza sistémica, pero suele ser más lenta y frágil.

Ningún nivel reemplaza a los otros. Se combinan según el riesgo que cada frontera introduce.

Cuando no se sabe la salida pero sí la propiedad

No siempre conocemos una salida exacta, pero sí propiedades. Ordenar debe conservar elementos y dejarlos en orden; serializar y deserializar debería recuperar el valor; agregar una entrada a un conjunto dos veces debería equivaler a agregarla una.

Generar muchas entradas a partir de esas propiedades descubre combinaciones que los ejemplos manuales no imaginaron y obliga a expresar qué significa que la operación sea correcta.

Depurar empieza por reproducir

Depurar empieza por reproducir. Después se reduce el caso, se observa el estado cerca del primer punto donde diverge y se formula una hipótesis falsable. Logs, breakpoints y trazas sirven para medir, no para reemplazar una pregunta concreta.

Corregir donde explota puede esconder la causa original. Hay que seguir el dato inválido hacia atrás hasta la frontera que debía impedirlo y agregar una prueba que falle antes del arreglo.

Código bajo prueba

const applyDiscount = (price, percent) => {
if (percent <= 0) return price;
if (percent > 100) percent = 100;
return price - (price * percent) / 100;
};

Metele un bug

El código está como lo escribió quien lo programó: toda la suite pasa.

La suite

  • pasasin descuento devuelve el precioapplyDiscount(100, 0) === 100
  • pasaaplica el 50 por cientoapplyDiscount(100, 50) === 50
  • pasadevuelve un númerotypeof resultado === "number"

Probá las cuatro. La prueba que no se pone en rojo con ninguna mutación no está probando nada: su nombre promete algo que nadie verifica.

Qué hace mala a una prueba

Las pruebas deben ser deterministas y controlar reloj, azar y servicios externos. Probar detalles privados vuelve costoso refactorizar sin aumentar garantías; conviene afirmar resultados observables.

La cobertura señala código no ejecutado, pero cien por ciento no asegura buenos casos. Importa qué riesgos se ejercitan y si un error real produciría una falla comprensible.

Un test que sirve y uno que no

// No sirve: prueba que el código hace lo que hace
test('applyDiscount devuelve un número', () => {
  expect(typeof applyDiscount(100, 10)).toBe('number');
});

// Sirve: fija una regla de negocio, con un caso que podría fallar
test('un descuento del 10 % sobre 100 deja el precio en 90', () => {
  expect(applyDiscount(100, 10)).toBe(90);
});

// Sirve más: el borde que nadie probó a mano
test('un descuento mayor a 100 se recorta al 100 y no devuelve plata', () => {
  expect(applyDiscount(100, 150)).toBe(0);
});

// El test de regresión que se escribe al arreglar un bug
test('un cupón vencido no aplica descuento (bug #482)', () => {
  expect(applyDiscount(100, 10, { expiresAt: yesterday })).toBe(100);
});

El primero pasa siempre, incluso con la función rota, y por eso no aporta nada más que una cifra de cobertura. Los otros tres fallan si alguien cambia el comportamiento, que es lo único que un test tiene que hacer.

El cuarto tiene un valor distinto: se escribe antes de arreglar el bug y se verifica que falle. Esa comprobación —que el test falle sin el arreglo— es lo que garantiza que el test está probando lo que uno cree.

Depurar sin adivinar

PasoQué se haceQué error evita
Reproducirencontrar la entrada mínima que fallaarreglar algo que no era
Bisecarpartir el espacio de causas al medioprobar cambios al azar
Formularescribir la hipótesis antes de tocar nadaconfundir correlación con causa
Verificarun cambio por vezno saber cuál de los tres lo arregló
Fijarun test que falle sin el arregloque vuelva dentro de seis meses
Bisecar es el paso que más tiempo ahorra y el que menos se usa: con veinte causas posibles, probar de a una son veinte intentos y partir al medio son cinco.

Cierre

Las pruebas convierten contratos en verificaciones repetibles. La depuración convierte síntomas en hipótesis y evidencia. Juntas forman un ciclo: reproducir, explicar, corregir en la causa y conservar una prueba que impida la regresión.

Autoevaluación

¿Lo entendiste?

¿Qué demuestra una prueba que pasa?
No se conoce la salida exacta de una operación. ¿Qué se puede probar igual?
¿Por dónde empieza una depuración?
Se encontró dónde explota. ¿Ahí se corrige?