📋 Guía de Aula Invertida
— vistas

Testing profundo: unitarias, mocks, cobertura y regresión

Ya sabés escribir pruebas a partir de los criterios de aceptación. Ahora el sistema crece: hay base de datos, hay correos, hay otros servicios. Esta guía enseña a probar cada pieza por separado sin depender de las demás, a leer un informe de cobertura sin dejarse engañar, a blindar un bug corregido para que no vuelva y, al final, a ver cómo cambia todo esto si el sistema se parte en microservicios.

RC
Ing. Roy Carrasco
Facultad de Ingeniería de Sistemas · UAB

Bienvenido

Camila, la Product Owner de EventoUAB, pidió algo nuevo para el Sprint 3: "Cuando una reserva queda confirmada, el club tiene que recibir un correo." Valentina lo implementa y se encuentra con un problema práctico: para probar reservar ahora necesitaría una base de datos y un servidor de correo funcionando. Cada corrida de pruebas mandaría correos de verdad.

Mateo le propone otro camino: "No pruebes todo junto. Probá la regla sola, y reemplazá lo que no te interesa por un sustituto de mentira." Ese es el hilo de la guía: aislar lo que se prueba para que las pruebas sean rápidas, repetibles y fáciles de diagnosticar.

Objetivo del módulo

Al finalizar esta guía sabrás ubicar cada prueba en la pirámide (unitaria, de integración, extremo a extremo), elegir entre stub, fake y espía según lo que quieras comprobar, interpretar un informe de cobertura sabiendo qué NO te dice, convertir un bug en una prueba de regresión, y explicar cómo cambia la estrategia de pruebas al pasar a microservicios.

📝 Actividades de este módulo

Encontrarás actividades cortas y autocorregibles —selección múltiple, clasificación, emparejamiento y completar espacios— repartidas a lo largo de la guía, y una sección de ejercicios de razonamiento donde se te da un escenario y debés decidir antes de comprobar.

🔺 Pirámide

Muchas pruebas rápidas y chicas, pocas lentas y grandes.

🎭 Dobles de prueba

Sustitutos que reemplazan la base de datos o el correo.

📊 Cobertura

Qué código ejecutaron las pruebas, y qué no comprobaron.

La pirámide de pruebas

No todas las pruebas son iguales. Se distinguen por cuánto del sistema recorren, y de eso dependen su velocidad, su costo y qué tan fácil es saber qué se rompió.

NivelQué recorreEjemplo en EventoUABVelocidad
UnitariaUna función o clase sola; lo demás se reemplaza.ReservasService.reservar con un repositorio en memoria.Milisegundos
De integraciónVarias piezas reales juntas.El servicio con el repositorio SQLite de verdad.Décimas de segundo a segundos
Extremo a extremoLa aplicación entera, como la usaría una persona.Abrir el navegador, reservar un espacio y ver la confirmación.Segundos a minutos
Por qué es una pirámide

Conviene tener muchas pruebas unitarias en la base (rápidas, baratas, señalan la línea exacta que falló), algunas de integración en el medio y pocas de extremo a extremo arriba (lentas, frágiles, pero las únicas que comprueban que todo encaja). Invertir la pirámide —casi todo extremo a extremo— produce una suite que tarda mucho, falla por motivos ajenos al código y nadie quiere correr.

Clasificación

¿En qué nivel de la pirámide está cada prueba de EventoUAB?

Comprueba la regla de solapamiento llamando a reservar con un repositorio en memoria.
Abre el sitio en un navegador automatizado, completa el formulario de reserva y espera el mensaje de éxito.
Guarda una reserva en un archivo SQLite real y la vuelve a leer con el repositorio de verdad.
Verifica que una hora de fin anterior al inicio lanza un error, sin tocar ninguna base de datos.
Levanta la API completa con su base de datos y hace una petición HTTP real a la ruta de reservas.

Stubs, fakes y espías

Para que una prueba sea unitaria, lo que rodea al código se reemplaza por un doble de prueba, como los dobles de las películas. Este es el servicio de EventoUAB con la nueva regla de Camila: depende de un repositorio y de un notificador, ambos recibidos de afuera (la inyección de dependencias de la guía anterior).

Node.js — reservas-service.js
export class ReservasService {
  constructor(repo, notificador) {
    this.repo = repo;
    this.notificador = notificador;
  }

  reservar(nueva) {
    if (nueva.fin <= nueva.inicio) {
      throw new Error("La hora de fin debe ser posterior al inicio");
    }
    const ocupado = this.repo
      .delEspacio(nueva.espacioId)
      .some((r) => r.inicio < nueva.fin && r.fin > nueva.inicio);
    if (ocupado) return false;
    this.repo.guardar(nueva);
    this.notificador.enviarConfirmacion(nueva.club);
    return true;
  }
}

Hay tres tipos de doble, y cada uno responde a una pregunta distinta:

DobleQué haceÚsalo cuando querés comprobar...
StubDevuelve respuestas fijas, enlatadas.Cómo reacciona tu código ante una situación concreta (espacio ocupado, error de la base).
FakeUna implementación real pero simplificada (una lista en memoria en vez de SQLite).Un comportamiento que depende del estado: guardar varias cosas y consultarlas.
Espía (mock)Anota cómo lo llamaron para preguntarlo después.Que el código hizo algo hacia afuera: enviar un correo, llamar a otro servicio.
Node.js — reservas-service.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { ReservasService } from "./reservas-service.js";

// Fake: una implementación real pero simple, en memoria
class ReservasEnMemoria {
  constructor() { this.todas = []; }
  delEspacio(id) { return this.todas.filter((r) => r.espacioId === id); }
  guardar(r) { this.todas.push(r); }
}

// Espía: no hace nada útil, solo anota cómo lo llamaron
class NotificadorEspia {
  constructor() { this.llamadas = []; }
  enviarConfirmacion(club) { this.llamadas.push(club); }
}

test("stub: con el espacio ocupado, rechaza", () => {
  const repoStub = {
    delEspacio: () => [{ espacioId: 1, inicio: 10, fin: 12 }],  // respuesta enlatada
    guardar: () => { throw new Error("no debería guardar"); },
  };
  const servicio = new ReservasService(repoStub, new NotificadorEspia());
  assert.equal(servicio.reservar({ espacioId: 1, club: "Robótica", inicio: 11, fin: 13 }), false);
});

test("fake: dos reservas contiguas conviven", () => {
  const servicio = new ReservasService(new ReservasEnMemoria(), new NotificadorEspia());
  assert.equal(servicio.reservar({ espacioId: 1, club: "A", inicio: 10, fin: 12 }), true);
  assert.equal(servicio.reservar({ espacioId: 1, club: "B", inicio: 12, fin: 14 }), true);
});

test("espía: una reserva exitosa avisa una sola vez al club", () => {
  const notificador = new NotificadorEspia();
  const servicio = new ReservasService(new ReservasEnMemoria(), notificador);
  servicio.reservar({ espacioId: 1, club: "Robótica", inicio: 10, fin: 12 });
  assert.deepEqual(notificador.llamadas, ["Robótica"]);
});

test("espía: una reserva rechazada no avisa a nadie", () => {
  const notificador = new NotificadorEspia();
  const servicio = new ReservasService(new ReservasEnMemoria(), notificador);
  servicio.reservar({ espacioId: 1, club: "A", inicio: 10, fin: 12 });
  servicio.reservar({ espacioId: 1, club: "B", inicio: 11, fin: 13 });
  assert.deepEqual(notificador.llamadas, ["A"]);
});
Ninguna prueba manda un correo ni abre una base de datos

Corren juntas en milisegundos y dan el mismo resultado en cualquier computadora. Fijate también en el stub: su guardar lanza un error. Si el servicio guardara una reserva que debía rechazar, la prueba fallaría sola.

Cuidado con el exceso de espías

Un espía comprueba qué hizo el código hacia afuera. Úsalo para efectos reales (el correo salió, y una sola vez). No lo uses para verificar el orden de llamadas internas ni cuántas veces se consultó el repositorio: esas pruebas se rompen cada vez que reorganizás el código por dentro aunque el comportamiento siga siendo correcto. Preferí comprobar el resultado sobre el procedimiento.

Emparejamiento

Une cada doble de prueba con lo que lo caracteriza.

Completar

Elegí el doble adecuado para cada necesidad de prueba.

Para probar cómo reacciona el servicio cuando el repositorio ya devuelve un horario ocupado, uso un ; para comprobar que el club recibió exactamente un correo, uso un ; y para guardar varias reservas seguidas y consultarlas después, uso un .

Razonamiento

Una prueba verifica que reservar llamó a repo.delEspacio exactamente dos veces y en cierto orden. Mateo reorganiza el código por dentro, el comportamiento sigue igual y la prueba falla. ¿Qué pasó?

Cobertura: qué dice y qué no dice

La cobertura es el porcentaje de código que las pruebas ejecutaron al correr. Node la calcula sin instalar nada: node --test --experimental-test-coverage. Es una herramienta útil, pero se interpreta mal con mucha facilidad.

Esta es una prueba de Valentina para "tener cobertura". Ejecuta reservar pero no comprueba absolutamente nada:

Node.js — cobertura.test.js
test("camino feliz, pero sin comprobar nada", () => {
  const s = new ReservasService(
    { delEspacio: () => [], guardar() {} },
    { enviarConfirmacion() {} }
  );
  s.reservar({ espacioId: 1, club: "A", inicio: 10, fin: 12 });   // ← ni un assert
});
Terminal — informe de cobertura
Solo la prueba sin asserts:
  file                | line % | branch % | funcs % | uncovered lines
  reservas-service.js |  89.47 |    60.00 |   66.67 | 9-10

Las 4 pruebas con asserts de la sección anterior:
  reservas-service.js |  89.47 |    85.71 |  100.00 | 9-10

Esas 4 + la prueba de "fin anterior al inicio":
  reservas-service.js | 100.00 |   100.00 |  100.00 |

Mirá la primera línea: una prueba que no comprueba nada alcanza el mismo 89,47 % de líneas que las cuatro pruebas bien escritas. El informe no distingue entre ejecutar el código y verificarlo. La columna "uncovered lines" sí es valiosa: señala las líneas 9-10, el error de "fin anterior al inicio", que ninguna prueba había tocado todavía.

Para ver la diferencia real, repetimos el experimento de la guía anterior: cambiar r.fin > nueva.inicio por r.fin >= nueva.inicio en la regla de solapamiento, a propósito.

Qué se corre contra el código con el errorResultado
La prueba sin asserts✔ pasa (no detecta nada)
Las 5 pruebas con asserts✖ falla "fake: dos reservas contiguas conviven" (4 pasan, 1 falla)
Cómo leer la cobertura

Una cobertura baja es información confiable: ese código no está probado. Una cobertura alta no garantiza nada: solo dice que el código se ejecutó, no que alguien comprobara su resultado. Úsala para encontrar lo que falta, no como meta. Un equipo que persigue "100 %" por sí mismo termina escribiendo pruebas sin asserts, justo como la de arriba.

Razonamiento

El informe de cobertura marca 100 % y aun así se cuela un error de comparación. ¿Cuál es la explicación más probable?

Completar

Completá lo que le pasó a la prueba sin asserts de Valentina.

Cuando mido la cobertura de una prueba que ejecuta reservar sin comprobar nada, las líneas ejecutadas figuran como ; si alguien cambia > por >=, esa prueba ; la cobertura mide .

Pruebas de regresión

Una regresión es algo que antes funcionaba y dejó de funcionar después de un cambio. Es el bug más frustrante, porque ya lo habías resuelto. Las pruebas automáticas existen, en buena parte, para atraparlas.

Escenario: en un apuro, alguien "simplifica" la regla de solapamiento y deja r.fin >= nueva.inicio. Una reserva que empieza justo cuando termina otra pasa a rechazarse. Si corrés la suite, el error aparece solo:

Terminal — node --test
✔ stub: con el espacio ocupado, rechaza
✖ fake: dos reservas contiguas conviven
✔ espía: una reserva exitosa avisa una sola vez al club
✔ espía: una reserva rechazada no avisa a nadie
✔ inválido: fin anterior al inicio lanza error
ℹ tests 5   ℹ pass 4   ℹ fail 1

Otro cambio del mismo estilo —avisar al club también cuando la reserva se rechaza— hace fallar a "espía: una reserva rechazada no avisa a nadie", y solo a esa. Cada error cae en la prueba que describe justo esa regla, y eso es lo que hace útil una suite de pruebas pequeñas y específicas.

Cuando el bug se escapa: el ciclo de regresión

Si un error llega a producción, no alcanza con corregirlo. Hay un orden que evita que vuelva: 1) escribí una prueba que reproduzca el bug y comprobá que falla; 2) corregí el código; 3) verificá que esa prueba pasa y que toda la suite sigue en verde; 4) dejá la prueba en el repositorio para siempre. El paso 1 importa: una prueba que nunca se vio fallar no prueba que detecte el bug.

Dónde encaja en la Definition of Done

La DoD de la guía anterior ya exigía "las pruebas pasan en main después de integrar". Es exactamente esta defensa: correr la suite completa en cada integración convierte cada regresión en un aviso inmediato, en vez de un reclamo del usuario semanas después.

Completar

Reconstruí el ciclo para corregir un bug sin que vuelva a aparecer.

Primero escribo una prueba que reproduce el bug y compruebo que ; después ; luego confirmo que ; y finalmente .

Razonamiento

Un usuario reporta un bug. Valentina lo corrige directamente y lo prueba a mano una vez. ¿Qué le falta para que sea una corrección con protección contra regresión?

Panorama: microservicios

Hasta acá EventoUAB es una sola aplicación. Muchas empresas, en cambio, parten el sistema en microservicios: servicios pequeños e independientes (reservas, notificaciones, usuarios...), cada uno con su propio código, su propia base de datos y su propio despliegue, que se hablan entre sí por red. No es un tema para dominar en una guía; sí para entender por qué cambia la forma de probar.

Una sola aplicación (monolito)Microservicios
Cómo se hablan las piezasLlamadas a funciones dentro del mismo programa.Peticiones por red (HTTP, colas de mensajes).
Pruebas unitariasIgual que hasta ahora.Igual: cada servicio se prueba por dentro con dobles.
Pruebas de integraciónPiezas reales en el mismo proceso.Hay que acordar el contrato entre servicios: qué pide uno y qué responde el otro.
Extremo a extremoLevantar una aplicación.Levantar varios servicios juntos: más lento, más frágil, más caro.
La pieza nueva: la prueba de contrato

Si el servicio de reservas llama al de notificaciones, ambos equipos deben ponerse de acuerdo en el formato del mensaje. En una prueba de contrato el consumidor declara qué espera recibir y el proveedor verifica, en su propia suite, que realmente lo cumple. Así detectan un cambio incompatible sin levantar ambos servicios juntos. Es la versión en red del "doble" que ya conocés: un acuerdo escrito que reemplaza al servicio real.

¿Conviene para EventoUAB?

Casi seguro que no, todavía. Los microservicios ayudan cuando hay varios equipos que necesitan desplegar sin esperarse, o partes del sistema con carga muy distinta. Con un equipo pequeño agregan problemas nuevos (red, versiones, contratos, más pruebas lentas) sin resolver un dolor que el equipo tenga. Es el mismo criterio que con Repository: se incorpora donde la evidencia muestra el problema, no por anticipado.

Clasificación

Para cada situación, ¿partir el sistema en microservicios ayuda o estorba?

Cuatro estudiantes trabajan en una sola aplicación y no tienen problemas para desplegar.
Tres equipos distintos pisan el mismo código y se bloquean entre sí cada vez que despliegan.
El envío de correos recibe mil veces más carga que el resto y necesita escalar por separado.
Se quiere hacer más rápida una suite de pruebas que ya es lenta, sin cambiar nada más.

Ejercicios de razonamiento

En estas preguntas no se te pide recordar una definición: se te da un escenario y debés razonar la respuesta antes de comprobarla.

Escenario

La suite de EventoUAB tiene 120 pruebas, casi todas de extremo a extremo con navegador. Tarda 25 minutos y falla a menudo "por nada": a veces el navegador no carga a tiempo. Nadie la corre antes de integrar.

Razonamiento

¿Qué cambio de estrategia tiene más sentido?

Otro escenario

Mateo revisa un Pull Request que sube la cobertura de 62 % a 94 %. Al leer las pruebas nuevas, ve que la mayoría llama a funciones y no tiene ningún assert.

Razonamiento

¿Cómo debería responder en la revisión?

Actividades de repaso

Un repaso integrador de todo el módulo. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.

0 / 0
actividades correctas en toda la guía
Selección múltiple

¿Por qué la base de la pirámide de pruebas debe tener muchas pruebas unitarias?

Verdadero o falso

Un informe de cobertura del 100 % garantiza que el código no tiene errores.

Selección múltiple

Querés comprobar que, al rechazar una reserva, el sistema no envía ningún correo. ¿Qué doble usás para el notificador?

Selección múltiple

Se corrige un bug reportado por un usuario. ¿Qué es lo primero que debería hacerse?

Cheat Sheet

TérminoIdea clave
Pirámide de pruebasMuchas unitarias (rápidas), algunas de integración, pocas de extremo a extremo (lentas y frágiles).
Prueba unitariaPrueba una pieza sola, con sus dependencias reemplazadas por dobles.
StubRespuestas fijas: para ver cómo reacciona tu código ante una situación.
FakeImplementación simple pero real (lista en memoria): para comportamientos con estado.
Espía (mock)Anota las llamadas: para comprobar que se hizo algo hacia afuera. No abusar de él para el orden interno.
CoberturaQué código se ejecutó. Baja = información confiable; alta = no garantiza nada sin asserts.
RegresiónAlgo que funcionaba y dejó de funcionar. Se previene con una prueba que reproduce el bug (primero falla), se corrige, y queda en la suite.
Prueba de contratoEn microservicios, acuerdo verificable entre el servicio que pide y el que responde.

¿Sabías que...?

🔺 La pirámide viene de Mike Cohn

La idea de la pirámide de pruebas se atribuye a Mike Cohn, en su libro Succeeding with Agile (2009), y sigue siendo la referencia para equilibrar tipos de prueba.

🧬 Existen las pruebas de mutación

El experimento de cambiar > por >= a propósito está automatizado en herramientas de mutation testing (como Stryker): modifican el código y miden cuántos cambios detectan tus pruebas.

🧭 Lo que viene en la próxima guía

Con la suite bajo control, toca refactorizar: cambiar la estructura del código sin cambiar su comportamiento, y apoyarte en estas mismas pruebas para saber que no rompiste nada.