El módulo anterior terminó con una confesión incómoda: nueve refactorizaciones sobre código que funcionaba —la firma de ListaBicicletas, el estado movido de sitio, el value de dos proveedores de contexto reescrito, las rutas partidas en nueve fragmentos, una capa de carga perezosa con sus fallos de red— y una única comprobación en todas ellas: abrir el navegador y mirar. Funcionó porque el proyecto es pequeño y porque la persona que tocaba el código recordaba qué había que mirar. Ninguna de esas dos condiciones sobrevive al crecimiento. Este módulo construye lo que faltaba: una red de seguridad automática que se ejecuta en segundos, que comprueba lo que un humano no puede recordar y que falla cuando el comportamiento cambia. Esta primera lección no escribe todavía pruebas de la aplicación: establece el criterio. Qué es una prueba, qué tipos hay, cuáles compensan en una interfaz de React, qué merece la pena probar de CicloUrbano y qué no, cuáles son las métricas que engañan, y cómo se deja el entorno listo para que las cuatro lecciones siguientes escriban código de prueba desde la primera línea.
Contenido
- El coste real de refactorizar sin pruebas
- Qué es una prueba automatizada: preparar, actuar, comprobar
- Las tres cosas que aporta una prueba
- Tipos de prueba: estática, unitaria, de integración y de extremo a extremo
- La pirámide de pruebas frente al trofeo de pruebas
- El principio rector del módulo: comportamiento, no implementación
- Qué merece la pena probar en CicloUrbano y qué no
- Falsos amigos: cobertura, pruebas frágiles y pruebas inestables
- Cómo se nombra una prueba
- Puesta en marcha del entorno: Vitest, jsdom y Testing Library
- La primera prueba: verificar que el entorno respira
- La estrategia del Módulo 9
- El coste real de refactorizar sin pruebas
Recupera el cierre del módulo 8 y piensa en lo que ocurrió realmente en cada una de esas nueve refactorizaciones. En cada una hubo un momento, después de guardar el fichero, en el que el estado del proyecto era desconocido. Se resolvió mirando la pantalla. Mirar la pantalla comprueba, como mucho, lo que hay en pantalla en ese instante: el catálogo con las cinco bicicletas y el filtro por tipo. No comprueba lo que estaba en la otra pestaña, ni el caso de la reserva con cero horas, ni qué pasa cuando json-server devuelve un 500, ni si el botón «Reservar» sigue deshabilitado para bici-003, que está en mantenimiento.
Esto se puede formular con precisión. Al tocar código, la pregunta es siempre la misma:
¿Qué comportamientos podría haber roto este cambio, y cómo sé que no los he roto?
Sin pruebas, la respuesta a la segunda mitad es «no lo sé», y el equipo la sustituye por dos estrategias, ambas malas:
- No tocar el código. El fichero que nadie se atreve a modificar acaba siendo el que más lo necesita. La deuda técnica no es el código malo: es el código malo que no se puede cambiar.
- Tocarlo y esperar. El fallo llega al usuario, que lo cuenta días después, cuando ya nadie recuerda el cambio que lo causó.
Una prueba automatizada convierte «no lo sé» en «lo compruebo en cuatro segundos». Esa es la diferencia, y es una diferencia de grado tan grande que cambia cómo se escribe el código: con red, se refactoriza a menudo y en pasos pequeños; sin red, se acumula y se reescribe de golpe.
flowchart LR
A["Cambio en el código"] --> B{"¿Hay pruebas?"}
B -- No --> C["Comprobación manual parcial"]
C --> D["Regresión no detectada"]
D --> E["El usuario informa del fallo"]
E --> F["Depuración sin contexto: días después"]
B -- Sí --> G["npm test · segundos"]
G --> H{"¿Falla algo?"}
H -- Sí --> I["Se corrige con el cambio aún fresco"]
H -- No --> J["Se integra con confianza"]
El eje horizontal de ese diagrama es tiempo, y es donde está el ahorro: el coste de arreglar un fallo crece con la distancia entre el momento en que se introduce y el momento en que se detecta.
- Qué es una prueba automatizada: preparar, actuar, comprobar
Una prueba automatizada es, sin ninguna magia, una función que ejecuta código de la aplicación y lanza un error si el resultado no es el esperado. Nada más. El ejecutor de pruebas se encarga de encontrar esas funciones, llamarlas, capturar los errores y presentar un informe.
Toda prueba, sea del tipo que sea, tiene tres partes. En la literatura anglosajona se conocen como Arrange, Act, Assert (AAA); en este curso las llamaremos preparar, actuar y comprobar.
| Parte | Qué hace | Ejemplo en CicloUrbano |
|---|---|---|
| Preparar | Construye el escenario: datos de entrada, estado inicial, dependencias simuladas | Un catálogo con bici-001 disponible y bici-003 en mantenimiento |
| Actuar | Ejecuta una sola acción: llamar a la función, pulsar el botón, enviar el formulario | Llamar a validarReserva({ bicicletaId: 'bici-003', … }, catalogo) |
| Comprobar | Afirma qué debería haber pasado | El resultado contiene un error en bicicletaId con el texto «no está disponible» |
// Estructura de cualquier prueba, con las tres partes marcadas
test('rechaza una bicicleta en mantenimiento', () => {
// 1. PREPARAR
const bicicletas = [{ id: 'bici-003', modelo: 'Carga Max', estado: 'mantenimiento' }];
const datos = { bicicletaId: 'bici-003', fechaInicio: '2030-01-01T10:00', horas: 2, condiciones: true };
// 2. ACTUAR
const errores = validarReserva(datos, bicicletas);
// 3. COMPROBAR
expect(errores.bicicletaId).toContain('no está disponible');
});Tres reglas que salen directamente de esta estructura y que conviene interiorizar antes de escribir nada:
- Una prueba, una acción. Si en el bloque de actuar hay tres cosas, cuando la prueba falle no sabrás cuál de las tres la rompió. Dividir.
- La preparación debe ser explícita y local. Un dato que viene de otro fichero, de una prueba anterior o de un estado compartido convierte el fallo en un misterio. Cada prueba construye su escenario.
- Comprobar el resultado, no los pasos. La prueba de arriba no comprueba que se haya llamado a
bicicletas.find; comprueba qué devuelve la función. Esto es el principio del apartado 6, en miniatura.
- Las tres cosas que aporta una prueba
Vale la pena separar los tres beneficios, porque cada uno justifica un tipo de prueba distinto y explica decisiones que se toman más adelante.
3.1. Permite refactorizar sin miedo
Es el beneficio principal y el que motiva este módulo. Una refactorización, por definición, cambia la implementación sin cambiar el comportamiento. Si tienes una batería de pruebas que describe el comportamiento y no la implementación, refactorizar se convierte en un procedimiento mecánico: cambias el código, ejecutas las pruebas, y si todas pasan, la refactorización es correcta.
Aplicado a lo que ya hiciste: si ListaBicicletas hubiera tenido una prueba que dijera «dadas cinco bicicletas, pinta cinco tarjetas, y al pulsar “Reservar” en la primera avisa con bici-001», el cambio de firma y la introducción de useCallback habrían quedado verificados en el acto. Y si el memo hubiera dejado de propagar un campo, la prueba habría fallado.
La contrapartida está en la letra pequeña: solo funciona si la prueba no depende de la implementación. Una prueba que comprueba nombres de estado interno o número de renders se rompe con cada refactorización, y entonces la red de seguridad se convierte en un lastre. De ahí la insistencia del apartado 6.
3.2. Es documentación ejecutable
Un fichero de pruebas bien escrito responde a la pregunta «¿qué hace esto exactamente?» mejor que cualquier comentario, por una razón simple: los comentarios mienten con el tiempo y las pruebas no, porque si mienten, fallan.
✓ validarReserva ✓ devuelve un objeto vacío cuando todos los datos son correctos ✓ exige elegir una bicicleta ✓ rechaza una bicicleta que no existe en el catálogo ✓ rechaza una bicicleta en mantenimiento ✓ rechaza una fecha de inicio anterior a ahora ✓ rechaza 0 horas y acepta 1 ✓ rechaza 25 horas y acepta 24 ✓ exige aceptar las condiciones
Esa salida es la especificación de las reglas de negocio de las reservas de CicloUrbano, generada por el propio código. Quien entre nuevo al equipo la lee en treinta segundos.
3.3. Detecta regresiones antes que el usuario
Una regresión es un comportamiento que funcionaba y ha dejado de funcionar. Es el tipo de fallo más caro, porque nadie lo busca: se descubre por accidente, normalmente en producción y normalmente en la parte de la aplicación en la que nadie estaba trabajando.
Las pruebas invierten esa dinámica: cada comportamiento que se prueba una vez queda vigilado para siempre, sin coste marginal. Es la única forma de que una aplicación pueda crecer sin que la superficie de riesgo crezca con ella.
- Tipos de prueba: estática, unitaria, de integración y de extremo a extremo
No todas las pruebas cuestan ni aportan lo mismo. Esta tabla es el mapa del módulo entero, y conviene volver a ella al empezar cada lección.
| Tipo | Qué prueba | Velocidad | Coste de mantenimiento | Confianza que aporta | Qué detecta que las otras no |
|---|---|---|---|---|---|
| Estática (ESLint, TypeScript) | El código sin ejecutarlo: sintaxis, tipos, reglas | Instantánea (mientras escribes) | Muy bajo | Baja pero gratuita | Erratas, variables no usadas, hooks mal llamados, props inexistentes |
| Unitaria | Una función o un módulo aislado | Muy alta (milisegundos) | Bajo si el módulo es puro | Baja: la unidad funciona, el conjunto no se sabe | Errores de lógica y casos límite difíciles de reproducir por la interfaz |
| De integración | Varias piezas juntas: un componente con sus hijos, su estado y sus proveedores | Alta (decenas o cientos de ms) | Medio | Alta: es como se usa de verdad | Cableado roto entre piezas que funcionan por separado |
| De extremo a extremo (e2e) | La aplicación entera en un navegador real | Baja (segundos por prueba) | Alto | Máxima | Fallos de enrutamiento real, CSS que tapa un botón, sesión, red real, varias pantallas encadenadas |
Algunas precisiones que suelen faltar:
- Las pruebas estáticas son pruebas. ESLint con
eslint-plugin-react-hooksdetecta hoy una familia entera de fallos que en 2018 requerían pruebas: unuseEffectcon dependencias incompletas, un hook dentro de unif. Configurarlo bien es la inversión más rentable del proyecto, y es requisito previo, no alternativa. - La frontera entre unitaria e integración es difusa, y no importa. Cuando se prueba
TarjetaBicicleta, que renderizaEtiquetaEstadodentro, ¿es unitaria o de integración? Es de integración, y es lo correcto. Discutir la etiqueta es tiempo perdido; lo que importa es el criterio del apartado siguiente. - La confianza no es proporcional al número de pruebas, sino a su tipo. Mil pruebas unitarias de funciones auxiliares no garantizan que la aplicación arranque. Una prueba e2e que reserva una bicicleta sí.
- La pirámide de pruebas frente al trofeo de pruebas
Durante dos décadas, el modelo dominante fue la pirámide de pruebas (Mike Cohn, 2009): muchas unitarias en la base, algunas de integración en medio, muy pocas de extremo a extremo en la cima. La justificación era económica: las unitarias eran baratas y rápidas; las de integración, lentas y frágiles.
flowchart TB
subgraph PIRAMIDE["Pirámide clásica"]
direction TB
P3["E2E · muy pocas"]
P2["Integración · algunas"]
P1["Unitarias · muchísimas"]
P3 --- P2 --- P1
end
subgraph TROFEO["Trofeo de pruebas (front-end moderno)"]
direction TB
T4["E2E · pocas, solo flujos críticos"]
T3["Integración · LA MAYORÍA"]
T2["Unitarias · las justas: lógica pura y casos límite"]
T1["Estática · linter y tipos, gratis y continua"]
T4 --- T3 --- T2 --- T1
end
Kent C. Dodds propuso el trofeo de pruebas para el front-end actual, y el cambio de forma responde a tres hechos concretos:
- Las pruebas de integración han dejado de ser caras.
jsdommonta un DOM completo en memoria en milisegundos, y Testing Library permite interactuar con él como lo haría una persona. Lo que en 2010 requería levantar un navegador hoy cuesta 50 ms. - El análisis estático ha absorbido gran parte de lo que probaban las unitarias. Un tipo mal pasado o un hook mal llamado ya no necesitan prueba: el linter y el compilador de tipos los cazan antes.
- En una interfaz, casi todos los fallos reales están en el cableado, no en las unidades. El componente funciona, el reductor funciona, el hook funciona… y la pantalla está vacía porque el selector devuelve
undefined. Las pruebas unitarias de cada pieza pasan las tres. La prueba de integración falla, que es lo que se quiere.
De ahí sale la regla operativa de este módulo, y la que se aplicará a CicloUrbano:
El peso cae en las pruebas de integración de componentes. Las unitarias se reservan para la lógica pura con casos límite —validaciones, reductores, selectores, utilidades— y las de extremo a extremo para tres o cuatro flujos críticos de negocio.
- El principio rector del módulo: comportamiento, no implementación
Si de este módulo solo te llevas una idea, que sea esta:
Prueba lo que el usuario ve y hace, no cómo lo has escrito por dentro.
La forma práctica de saber si una prueba respeta el principio es la prueba de la refactorización: si puedes reescribir el interior del componente sin cambiar nada de lo que el usuario percibe, y la prueba falla, la prueba está mal.
Míralo con dos versiones de la misma comprobación sobre SelectorTipo. Aquí solo interesa el contraste; la mecánica de render y screen es materia de 09-03.
// ❌ MAL: comprueba la implementación
test('actualiza el estado tipoElegido al pulsar Eléctrica', () => {
const componente = montarYEspiarEstado(<SelectorTipo />);
componente.pulsar('Eléctrica');
// Se afirma sobre el NOMBRE de una variable interna
expect(componente.estado.tipoElegido).toBe('electrica');
});// ✅ BIEN: comprueba el comportamiento observable
test('marca el filtro pulsado como activo y avisa del tipo elegido', async () => {
const usuario = userEvent.setup();
const alCambiarTipo = vi.fn();
render(<SelectorTipo tipo="todos" alCambiarTipo={alCambiarTipo} />);
await usuario.click(screen.getByRole('button', { name: 'Eléctrica' }));
expect(alCambiarTipo).toHaveBeenCalledWith('electrica');
});Ahora aplica un cambio perfectamente legítimo: renombrar tipoElegido a tipoActivo, o sustituir el useState por un useReducer, o —como ocurrió de verdad en el módulo 7— subir ese estado a Redux y pasar el tipo por props.
| Cambio en la implementación | La prueba «MAL» | La prueba «BIEN» |
|---|---|---|
Renombrar tipoElegido → tipoActivo |
Falla (falso positivo) | Pasa |
useState → useReducer |
Falla | Pasa |
| Estado subido a Redux / a la URL | Falla | Pasa |
| El botón deja de avisar al padre | Pasa (¡fallo real no detectado!) | Falla ✅ |
Esa última fila es la más elocuente: la prueba acoplada a la implementación no solo se rompe cuando no debe, sino que deja pasar el fallo que sí importa. Falla cuando no toca y calla cuando toca. Es peor que no tener prueba, porque además consume tiempo de mantenimiento.
La lista de cosas que no se prueban nunca, y que se repetirá en 09-03 con ejemplos:
- El estado interno de un componente y el nombre de sus variables.
- Los nombres de las clases de CSS (
estilos.activoes un hash generado por CSS Modules). - Cuántas veces se ha renderizado un componente (para eso está el Profiler de 08-05, que es una herramienta de diagnóstico, no de verificación).
- Que se haya llamado a un hook concreto o a un método concreto de una biblioteca.
- Métodos privados y funciones no exportadas: si merecen prueba, es que merecen ser exportadas.
- Qué merece la pena probar en CicloUrbano y qué no
Probarlo todo es imposible y probar al azar es inútil. El criterio que funciona combina dos ejes: cuánto duele si se rompe y cuánto cuesta la prueba. Aplicado al código que ya existe en el proyecto:
| Tipo de código de CicloUrbano | Ejemplos concretos | ¿Se prueba? | Con qué tipo de prueba | Lección |
|---|---|---|---|---|
| Utilidades puras | validarReserva, disponibilidad.js, clases |
Sí, a fondo | Unitaria, con todos los casos límite | 09-02 |
| Reductores y selectores | sliceReservas, sliceCatalogo, createSelector |
Sí, a fondo | Unitaria: son funciones puras, no necesitan React | 09-02 |
| Componentes de presentación | EtiquetaEstado, TarjetaBicicleta, Panel |
Sí, lo justo | Integración ligera: qué pinta y qué avisa | 09-03 |
| Formularios con reglas | FormularioReserva |
Sí, prioritario | Integración: rellenar, enviar, ver errores | 09-03 |
| Hooks personalizados | useAlternar, useDebounce, useAlmacenLocal |
Sí, si tienen lógica propia | Unitaria con renderHook |
09-04 |
| Páginas con datos del servidor | PaginaCatalogo, PaginaDetalleEstacion |
Sí, los tres estados | Integración con la red simulada (MSW) | 09-04 |
| Mutaciones | useCrearReserva y su invalidación |
Sí | Integración con MSW | 09-04 |
| Flujos completos de negocio | Identificarse · reservar · cancelar | Sí, solo esos tres | Extremo a extremo con Cypress | 09-05 |
| Estilos y maquetación | CSS Modules, variables :root, tema oscuro |
No con pruebas de código | Revisión visual; a lo sumo, regresión visual | — |
| Código de terceros | React Router, TanStack Query, Redux Toolkit | No | Ya están probados por sus autores | — |
| Envoltorios sin lógica | Panel si solo pinta children dentro de un <section> |
No merece la pena | — | — |
| Constantes y datos ficticios | datos/dominio.js |
No | Probar una constante es tautológico | — |
Dos criterios más para desempatar cuando dudes:
- ¿Ha fallado alguna vez? Cada fallo real que llega a producción merece una prueba que lo reproduzca antes de arreglarlo. Esa prueba es la que garantiza que no vuelva.
- ¿Está en el camino del dinero? En CicloUrbano, crear una reserva es el camino del dinero. Cambiar el tema a oscuro no lo es. El presupuesto de pruebas se reparte en consecuencia.
- Falsos amigos: cobertura, pruebas frágiles y pruebas inestables
8.1. La cobertura como métrica
La cobertura de código (code coverage) mide qué porcentaje del código se ha ejecutado durante las pruebas, normalmente desglosado en cuatro dimensiones:
| Métrica | Qué mide |
|---|---|
| Líneas (lines) | Porcentaje de líneas ejecutadas |
| Sentencias (statements) | Igual, pero por sentencia; difiere cuando hay varias en una línea |
| Ramas (branches) | Porcentaje de caminos de if, ?:, &&, switch recorridos. La más informativa |
| Funciones (functions) | Porcentaje de funciones llamadas al menos una vez |
Para lo que sirve de verdad: para encontrar lo que no has probado. Abres el informe, ves que la rama del error 500 de PaginaCatalogo está en rojo, y decides si merece una prueba. Ese uso es excelente.
Para lo que no sirve: como objetivo. Esta prueba da 100 % de cobertura de validarReserva y no comprueba absolutamente nada:
// 100 % de cobertura, cero valor
test('validarReserva no explota', () => {
validarReserva({ bicicletaId: 'bici-001', fechaInicio: '2030-01-01T10:00', horas: 2, condiciones: true }, []);
});Ejecuta la función, recorre líneas… y no tiene ni una sola aserción. La cobertura mide ejecución, no verificación. Por eso el 100 % no es el objetivo: perseguirlo empuja a escribir pruebas de relleno para envoltorios triviales y a probar código que no lo merece, y el tiempo sale del presupuesto de las pruebas que sí importan. Un rango razonable en un proyecto sano está entre el 70 % y el 85 %, con una condición: que ese porcentaje incluya la lógica de negocio. Un 90 % que deja validarReserva sin cubrir vale menos que un 60 % que la cubre entera.
8.2. Pruebas frágiles
Una prueba frágil (brittle) es la que falla ante cambios que no rompen nada. Es exactamente la del apartado 6, y sus causas habituales son siempre las mismas:
- Aserciones sobre detalles internos (estado, clases CSS, estructura del DOM exacta).
- Selectores acoplados a la maquetación: «el tercer
<div>dentro del segundo<section>». - Textos completos y literales con puntuación incluida, que se rompen al corregir una tilde.
- Dependencia del orden entre pruebas: la prueba B solo pasa si antes se ejecutó la A.
El síntoma reconocible: en cada pull request hay que «arreglar las pruebas». Cuando eso pasa, el equipo deja de creerse los fallos, y una prueba en la que nadie cree ya no protege nada.
8.3. Pruebas inestables (flaky)
Una prueba inestable es la que pasa o falla con el mismo código, sin cambiar nada. Es el peor de los males, porque destruye la única propiedad que hace útil a una suite: que un fallo signifique algo.
| Causa habitual | Cómo se manifiesta | Solución correcta |
|---|---|---|
Esperar con un tiempo fijo (setTimeout(500)) |
Falla en una máquina lenta o en integración continua | Esperar a que aparezca lo que se espera, con findBy* / waitFor (09-04) |
| Estado compartido entre pruebas | Falla solo al ejecutar la suite entera, no en solitario | Aislar: caché nueva, almacén nuevo, localStorage limpio en cada prueba |
| Dependencia del reloj real | Falla a medianoche o al cambiar la hora | Temporizadores falsos y fechas fijas (09-02) |
| Dependencia del orden de ejecución | Falla al ejecutar en paralelo | Cada prueba construye su escenario completo |
| Red real en una prueba | Falla cuando la API va lenta o está caída | Interceptar la red con MSW (09-04) |
Y la regla más importante sobre ellas: una prueba inestable no se reintenta, se arregla o se borra. Marcarla como «reintentar tres veces» esconde un fallo real de aislamiento que, tarde o temprano, será un fallo de la aplicación.
- Cómo se nombra una prueba
El nombre de una prueba es lo único que se ve cuando falla en un registro de integración continua a las tres de la mañana. Tiene que bastar para entender qué se ha roto, sin abrir el fichero.
La fórmula que mejor funciona describe el comportamiento esperado, no el mecanismo:
<sujeto>+ qué hace + en qué circunstancia
| ❌ Nombre pobre | ✅ Nombre útil |
|---|---|
test('funciona') |
test('devuelve un objeto vacío cuando todos los datos son válidos') |
test('validarReserva 2') |
test('rechaza una reserva de 25 horas porque supera el máximo') |
test('render') |
test('muestra el botón Reservar deshabilitado si la bicicleta está en mantenimiento') |
test('setState') |
test('avisa al padre con el tipo elegido al pulsar un filtro') |
Convenciones del proyecto, para que las cinco lecciones sean coherentes:
- Los nombres van en español, como todos los textos del proyecto;
describe,testyexpectno se traducen, son API. - Los
describeagrupan por sujeto: el nombre del módulo o del componente, tal cual (describe('validarReserva', …),describe('TarjetaBicicleta', …)). - Nada de «debería»:
test('rechaza…')en presente de indicativo. Es más corto y se lee mejor en el informe. - Un
describeanidado cuando comparten circunstancia:describe('cuando la bicicleta está en mantenimiento', …).
- Puesta en marcha del entorno: Vitest, jsdom y Testing Library
CicloUrbano se construye con Vite, y el ejecutor de pruebas natural para un proyecto Vite es Vitest: comparte la misma configuración, el mismo sistema de resolución de módulos y las mismas transformaciones, así que no hay una segunda cadena de compilación que mantener. Y —esto importa para la lección siguiente— implementa la misma API que Jest, el estándar del ecosistema.
10.1. Instalación
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event| Paquete | Para qué |
|---|---|
vitest |
El ejecutor: encuentra los ficheros de prueba, los ejecuta y da el informe |
jsdom |
Implementación del DOM en JavaScript: da un document y un window fuera del navegador |
@testing-library/react |
render, screen y las consultas para probar componentes como los usa una persona |
@testing-library/jest-dom |
Matchers específicos del DOM: toBeInTheDocument, toBeDisabled, toHaveValue… |
@testing-library/user-event |
Simula interacciones reales: clic, escritura, tabulación |
Todo va en devDependencies (-D): no forma parte del paquete que se despliega.
10.2. Configuración en vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// 1) Entorno: un DOM en memoria, porque probamos componentes
environment: 'jsdom',
// 2) describe / test / expect disponibles sin importarlos, como en Jest
globals: true,
// 3) Fichero que se ejecuta UNA VEZ antes de cada archivo de pruebas
setupFiles: './src/pruebas/configuracion.js',
// 4) Qué se incluye en el informe de cobertura
coverage: {
reporter: ['text', 'html'],
include: ['src/**/*.{js,jsx}'],
exclude: ['src/pruebas/**', 'src/main.jsx', 'src/**/*.test.{js,jsx}']
}
}
});Apartado por apartado:
environment: 'jsdom'es lo que permite querender(<TarjetaBicicleta />)tenga undocumentdonde pintar. Sin él, el entorno por defecto esnodey cualquier acceso al DOM lanzadocument is not defined. Para probar solo funciones puras,nodees más rápido; se puede fijar por fichero con un comentario// @vitest-environment node.globals: truehace quedescribe,test,expect,beforeEachyviestén disponibles sin importarlos. Es lo que da compatibilidad de código con Jest y lo que permite que@testing-library/jest-domse enganche. Sin esta opción hay que importar todo devitesten cada fichero.setupFilesapunta al fichero de preparación común, que es el siguiente apartado. Ahí se registran los matchers y, en 09-04, se arrancará el servidor simulado de MSW.coverageexcluye lo que no tiene sentido medir: las propias utilidades de prueba, el punto de entradamain.jsx(que solo llama acreateRoot) y los ficheros.test.
10.3. El fichero de preparación: src/pruebas/configuracion.js
// src/pruebas/configuracion.js
// Se ejecuta antes de CADA fichero de pruebas (setupFiles en vite.config.js)
// 1) Registra los matchers de DOM: toBeInTheDocument, toBeDisabled, toHaveValue…
import '@testing-library/jest-dom/vitest';
// 2) Comodidades globales del entorno de pruebas de CicloUrbano
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
// Testing Library limpia sola si `globals: true`, pero dejarlo explícito
// documenta la intención y protege ante un cambio de configuración.
afterEach(() => {
cleanup();
localStorage.clear(); // aísla las pruebas de useAlmacenLocal
});La importación es @testing-library/jest-dom/vitest, con el sufijo: esa variante llama a expect.extend del expect de Vitest. Si importas @testing-library/jest-dom a secas en Vitest, los matchers pueden no registrarse y toBeInTheDocument aparecerá como «no es una función».
localStorage.clear() en el afterEach no es adorno: useAlmacenLocal y ProveedorTema escriben ahí, y sin limpieza una prueba heredaría el tema oscuro que dejó la anterior. Es prevención directa del punto 8.3.
10.4. Estructura de ficheros del módulo
Los ficheros de prueba se colocan junto al código que prueban, y las utilidades compartidas en una carpeta propia:
src/
├── componentes/
│ ├── TarjetaBicicleta.jsx
│ ├── TarjetaBicicleta.test.jsx ← junto al componente
│ ├── FormularioReserva.jsx
│ └── FormularioReserva.test.jsx
├── utilidades/
│ ├── validarReserva.js
│ └── validarReserva.test.js
├── hooks/
│ ├── useDebounce.js
│ └── useDebounce.test.js
└── pruebas/ ← utilidades compartidas, sin pruebas propias
├── configuracion.js (10.3, y MSW en 09-04)
├── utilidades.jsx (el `renderizar` con proveedores, 09-03)
├── manejadores.js (manejadores de MSW, 09-04)
└── servidor.js (setupServer de MSW, 09-04)Colocar la prueba al lado del código, y no en un __tests__ lejano, tiene tres ventajas medibles: se ve de un vistazo qué está probado y qué no, la ruta de importación es './TarjetaBicicleta.jsx' en lugar de '../../componentes/TarjetaBicicleta.jsx', y al mover o borrar un componente la prueba viaja o desaparece con él.
10.5. Los scripts de package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"api": "json-server --watch db.json --port 3001",
"test": "vitest",
"test:ejecutar": "vitest run",
"test:ui": "vitest --ui",
"cobertura": "vitest run --coverage"
}
}| Script | Qué hace | Cuándo se usa |
|---|---|---|
npm test |
Modo vigilancia: se queda escuchando y reejecuta solo lo afectado al guardar | Mientras programas. Es el modo por defecto de Vitest |
npm run test:ejecutar |
Una pasada y sale con código 0 o 1 | Integración continua y ganchos de pre-commit |
npm run test:ui |
Interfaz web con el árbol de pruebas, el DOM y los tiempos | Depurar un fallo que no se entiende leyendo la consola |
npm run cobertura |
Genera el informe en consola y en coverage/index.html |
De vez en cuando, para buscar huecos (apartado 8.1) |
test:ui requiere npm install -D @vitest/ui, y la cobertura requiere npm install -D @vitest/coverage-v8. Vitest lo pide por consola la primera vez que ejecutas el script.
- La primera prueba: verificar que el entorno respira
Antes de escribir una sola prueba de la aplicación, conviene confirmar que la tubería funciona. Esta prueba no comprueba CicloUrbano: comprueba Vitest, jsdom y los matchers.
// src/pruebas/entorno.test.js
import { describe, test, expect } from 'vitest';
describe('entorno de pruebas', () => {
test('el ejecutor de pruebas funciona', () => {
expect(2 + 2).toBe(4);
});
test('jsdom proporciona un DOM en memoria', () => {
// Si `environment` no fuera 'jsdom', esta línea lanzaría "document is not defined"
document.body.innerHTML = '<h1>CicloUrbano</h1>';
expect(document.querySelector('h1')).toBeInTheDocument();
expect(document.querySelector('h1')).toHaveTextContent('CicloUrbano');
});
});Qué verifica cada una:
- La primera comprueba que Vitest encuentra el fichero, lo ejecuta y evalúa una aserción. Si esta falla, el problema está en la configuración, no en tu código.
- La segunda comprueba dos cosas a la vez: que
environment: 'jsdom'está activo, porque existedocument; y que los matchers dejest-domse han registrado, porquetoBeInTheDocumentytoHaveTextContentno son de Vitest, vienen del fichero de preparación.
$ npm test
✓ src/pruebas/entorno.test.js (2)
✓ entorno de pruebas (2)
✓ el ejecutor de pruebas funciona
✓ jsdom proporciona un DOM en memoria
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 09:14:32
Duration 412msCon esa salida en pantalla, el entorno está listo. Esta prueba puede borrarse después —no aporta nada a largo plazo—, pero es un buen primer paso, porque separa «mi prueba está mal» de «mi configuración está mal», que son dos problemas muy distintos y se depuran de forma muy distinta.
- La estrategia del Módulo 9
Este es el plan, y responde punto por punto al hueco que dejó el módulo 8:
| Lección | Qué añade | Sobre qué código de CicloUrbano |
|---|---|---|
| 09-01 (esta) | Criterio, tipos de prueba, entorno funcionando | La configuración |
| 09-02 | El ejecutor, las aserciones y los dobles de prueba; funciones puras, sin React | validarReserva, sliceReservas y sus selectores, monitorizacion.js |
| 09-03 | Componentes con Testing Library: consultas, interacción, proveedores | EtiquetaEstado, TarjetaBicicleta, SelectorTipo, FormularioReserva |
| 09-04 | Asincronía, temporizadores, red simulada con MSW y hooks personalizados | PaginaCatalogo, useCrearReserva, useDebounce, useAlmacenLocal, LimiteDeError |
| 09-05 | Extremo a extremo en un navegador real con Cypress, e integración continua | Los tres flujos críticos y el flujo de trabajo completo |
Y en el módulo 11, la lección 11-04 aplicará todo esto al proyecto final. Aquí se aprende la técnica; allí se ejecuta sobre una aplicación completa.
Errores Comunes y Consejos
- Escribir pruebas para subir la cobertura. Es el error que más tiempo consume y menos aporta. La cobertura es un mapa para encontrar huecos, no una nota. Un 72 % con la lógica de negocio cubierta vale más que un 95 % lleno de pruebas de envoltorios.
- Probar el estado interno «porque es más fácil». Suele serlo, y es exactamente la razón por la que hay que resistirse: una prueba fácil que se rompe en cada refactorización tiene coste negativo. Si probar el comportamiento cuesta mucho, casi siempre significa que el componente hace demasiadas cosas; la señal es útil.
- Empezar por las pruebas de extremo a extremo. Son las que más confianza dan y las más caras. Empezar por ahí produce una suite lenta e inestable que el equipo acaba desactivando. Se empieza por lo puro y por la integración de componentes; e2e al final y solo para los flujos críticos.
- Confundir «no tiene pruebas» con «hay que probarlo todo ya». En un proyecto existente, la estrategia que funciona es: prueba cada fallo antes de arreglarlo, prueba cada funcionalidad nueva, y prueba las zonas que tocas. La cobertura crece por donde el proyecto se mueve, que es donde hace falta.
- Dejar pruebas comentadas o con
test.skippermanente. Una prueba desactivada es una mentira almacenada: parece que hay red donde no la hay. Se arregla o se borra; el historial de Git guarda lo que se borró. - No ejecutar las pruebas en integración continua. Una suite que solo se ejecuta cuando alguien se acuerda no protege nada. En 09-05 verás el flujo de trabajo mínimo de GitHub Actions que las lanza en cada cambio.
- Consejo: instala primero, escribe después. Deja el entorno del apartado 10 funcionando con la prueba trivial del 11 antes de intentar probar nada real. Depurar una configuración y una prueba al mismo tiempo multiplica el tiempo por tres.
- Consejo: cuando una prueba falle, léela como si fuera un informe de fallo. Si el nombre y el mensaje no te dicen qué se ha roto, el problema es la prueba. Mejórala en ese momento, cuando tienes el contexto en la cabeza.
Ejercicios
Ejercicio 1. Clasifica cada uno de estos comportamientos de CicloUrbano según el tipo de prueba que le corresponde (estática, unitaria, de integración o de extremo a extremo) y justifica por qué no le corresponde el nivel inmediatamente inferior:
a) validarReserva rechaza una reserva de 25 horas.
b) FormularioReserva muestra el mensaje «La reserva máxima es de 24 horas» junto al campo de duración cuando se escribe 25 y se envía.
c) Ana Ribera entra en /acceso, reserva bici-001 y ve la reserva en /reservas.
d) useEffect en BuscadorBicicletas declara alBuscar en sus dependencias.
e) seleccionarBicicletasVisibles devuelve las bicicletas urbanas ordenadas por precio cuando el estado tiene tipo: 'urbana' y orden: 'precio'.
Ejercicio 2. Un compañero presenta esta prueba de TarjetaBicicleta y afirma que da confianza porque cubre el 100 % del componente. Identifica cuatro problemas distintos y reescribe el enunciado de las pruebas que la sustituirían (solo los nombres, no la implementación).
test('TarjetaBicicleta', () => {
const { container } = render(
<TarjetaBicicleta bicicleta={{ id: 'bici-001', modelo: 'Urbana Clásica', estado: 'disponible', precioHora: 2.5 }} />
);
expect(container.querySelector('.tarjeta_a3f9x')).toBeTruthy();
expect(container.querySelectorAll('button').length).toBe(2);
expect(container.innerHTML).toContain('Urbana Clásica');
});Ejercicio 3. El equipo de CicloUrbano tiene una prueba que falla aproximadamente una de cada diez ejecuciones en integración continua y siempre pasa en local. Comprueba que, tras crear una reserva, aparece el aviso «Reserva creada». Un miembro del equipo propone envolverla en retry(3). Explica por qué es mala idea, enumera tres causas posibles de la inestabilidad dadas las herramientas del proyecto, y describe qué comprobarías primero y por qué.
Soluciones
Solución 1.
| Caso | Tipo | Por qué no el nivel inferior |
|---|---|---|
| a | Unitaria | Es lógica pura sin DOM ni React. Bajar más solo dejaría el análisis estático, que no puede saber que el máximo son 24 horas: eso es una regla de negocio, no un tipo |
| b | Integración | Una unitaria de validarReserva confirma que el error se genera, pero no que el formulario lo muestre ni que lo asocie al campo correcto. El fallo típico aquí es de cableado: el error existe y no se pinta porque tocados no se actualizó |
| c | Extremo a extremo | Encadena tres pantallas, navegación real, sesión y persistencia. Una prueba de integración de cada página pasaría aunque la redirección tras el acceso llevara a la ruta equivocada |
| d | Estática | eslint-plugin-react-hooks lo detecta al guardar, gratis y sin ejecutar nada. Escribir una prueba para esto sería tirar el dinero |
| e | Unitaria | Un selector es una función pura de (estado) => resultado. Montar componentes para verificarlo añade coste, ruido en el diagnóstico y ninguna confianza adicional |
Solución 2. Los cuatro problemas:
- El nombre no describe nada.
test('TarjetaBicicleta')no dice qué comportamiento se verifica, así que su fallo en integración continua no informa. Y eldescribedebería llevar ese nombre, no eltest. - Aserción sobre una clase de CSS Modules.
.tarjeta_a3f9xes un identificador generado: cambia en cada compilación y con cada versión de Vite. Además, que exista undivcon esa clase no es un comportamiento que el usuario perciba. - Contar botones con
querySelectorAll. Se comprueba la estructura del DOM, no la función. Si se añade un tercer botón legítimo, la prueba falla sin que nada esté roto; y si el botón «Reservar» deja de estar deshabilitado en mantenimiento —el fallo real que importa—, la prueba sigue pasando. - Varias comprobaciones inconexas en una sola prueba y sin
screen. Se usancontainer.querySelectoreinnerHTMLen lugar de las consultas accesibles, y al fallar solo se sabe que «TarjetaBicicleta» falla, no cuál de las tres cosas.
Los enunciados que la sustituyen:
describe('TarjetaBicicleta')
test('muestra el modelo, la estación y el precio por hora')
test('permite reservar una bicicleta disponible y avisa con su identificador')
test('deshabilita el botón Reservar cuando la bicicleta está en mantenimiento')
test('avisa con el identificador al pulsar «Ver ficha»')Solución 3. Por qué retry(3) es mala idea: no arregla nada, esconde el síntoma y, sobre todo, desactiva la señal. Si la prueba falla una de cada diez veces por una condición de carrera real, esa misma condición de carrera afecta a los usuarios; taparla con reintentos convierte un fallo de producción detectado en un fallo de producción invisible. Además contamina el criterio del equipo: en cuanto se acepta que «a veces fallan», nadie vuelve a mirar un fallo rojo.
Tres causas posibles con las herramientas del proyecto:
- Espera por tiempo en lugar de por resultado. Si la prueba usa un retardo fijo tras enviar el formulario, en la máquina más lenta de integración continua la mutación aún no ha resuelto cuando se comprueba el aviso. Se corrige esperando al aviso con
findByText, que reintenta hasta que aparece (09-04). - Estado compartido entre pruebas. Un
QueryClientreutilizado entre ficheros deja en caché la lista de reservas de una prueba anterior; según el orden de ejecución, la lista ya contiene la reserva y el aviso nunca se dispara. Se corrige creando un cliente nuevo por prueba conretry: false. - La red no está interceptada. Si la prueba llega a
json-serverde verdad, depende de que el proceso esté levantado, de su latencia y de quedb.jsonno haya sido modificado por otra prueba. Se corrige con MSW.
Qué comprobaría primero: ejecutar el fichero en solitario y luego dentro de la suite completa. Es el diagnóstico más barato y separa de golpe las dos familias de causas. Si pasa en solitario y falla en la suite, el problema es de aislamiento (causas 2 y 3). Si falla en ambos casos de forma intermitente, el problema es de espera (causa 1). Sin esa bifurcación, cualquier corrección es a ciegas.
Conclusión
Esta lección ha puesto el criterio antes que la herramienta, que es el orden correcto: sin criterio, una batería de pruebas se convierte en un lastre que hay que «arreglar» en cada pull request.
Lo esencial que te llevas. Una prueba automatizada no es más que una función con tres partes —preparar, actuar y comprobar— y aporta tres cosas distintas: permite refactorizar sin miedo (el hueco exacto que dejaron las nueve refactorizaciones del módulo 8), sirve de documentación ejecutable que no puede mentir sin fallar, y detecta regresiones antes de que las cuente un usuario. Hay cuatro niveles —estática, unitaria, de integración y de extremo a extremo— y cada uno detecta fallos que los demás no ven: el linter caza el hook mal declarado, la unitaria el caso límite de 25 horas, la de integración el cableado roto entre piezas que funcionan por separado, y la e2e el CSS que tapa el botón de reservar. El reparto de peso ya no es la pirámide clásica sino el trofeo: en front-end moderno la mayoría de las pruebas son de integración, porque jsdom las ha abaratado, porque el análisis estático ha absorbido buena parte de las unitarias y porque ahí es donde están los fallos de verdad.
Por encima de todo queda el principio rector del módulo: se prueba el comportamiento visible, no la implementación. La prueba que afirma sobre tipoElegido falla al renombrar una variable y calla cuando el componente deja de avisar al padre —falla cuando no toca y calla cuando toca—; la que afirma que alCambiarTipo recibe 'electrica' sobrevive a useState, a useReducer y a subir el estado a Redux, y se rompe solo si se rompe algo. De ahí sale la lista de lo que no se prueba nunca: estado interno, clases de CSS Modules, número de renders y detalles de bibliotecas de terceros. Y el reparto para CicloUrbano: a fondo las utilidades puras, los reductores y los selectores; con integración los componentes y el formulario; con MSW las páginas con datos; con Cypress solo tres flujos críticos; y nada para los estilos, las constantes y el código de terceros.
Conoces también los falsos amigos. La cobertura es un mapa para encontrar huecos, no un objetivo: mide ejecución, no verificación, y una prueba sin una sola aserción puede dar el 100 %. Las pruebas frágiles fallan ante cambios inocuos y acaban con la credibilidad de la suite. Las inestables son peores, porque destruyen el significado del color rojo, y no se reintentan: se arreglan o se borran. Y sabes nombrar una prueba para que su fallo se entienda solo, con la fórmula sujeto + qué hace + en qué circunstancia.
Por último, el entorno está montado y verificado: Vitest con environment: 'jsdom', globals: true y setupFiles: './src/pruebas/configuracion.js'; los matchers de @testing-library/jest-dom/vitest registrados; localStorage limpio entre pruebas; los ficheros .test.jsx junto al código y las utilidades compartidas en src/pruebas/; y los scripts npm test, npm run test:ui y npm run cobertura. La prueba trivial de entorno.test.js confirma en 400 ms que la tubería entera funciona, que es lo que separa «mi prueba está mal» de «mi configuración está mal».
Con el criterio fijado y el entorno respirando, toca escribir pruebas de verdad. La próxima lección se queda deliberadamente fuera de React: el ejecutor por dentro, la anatomía de un fichero de pruebas con describe y sus ganchos, el catálogo de aserciones —empezando por la diferencia entre toBe y toEqual, que causa más confusión que ninguna otra—, los dobles de prueba con vi.fn() y vi.mock, y el control del tiempo con temporizadores falsos. Todo ello aplicado a validarReserva y al reductor sliceReservas, cerrando por fin lo que se prometió en 05-05 y 07-04: que un reductor puro se prueba llamándolo. Y como el índice manda, se hará desde el modelo de Jest, con la tabla de equivalencias que te permitirá llevarte lo aprendido a cualquier proyecto. La próxima lección es Pruebas Unitarias con Jest.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
