Cómo prevenir regresiones de UI con pruebas de snapshot de Jest en Angular

Hace unos días estaba corrigiendo un pequeño problema de accesibilidad y decidí eliminar algunos contenedores (wrappers) innecesarios alrededor de ciertos elementos. Después de hacer esos cambios, todas las pruebas pasaron con éxito, incluidas las pruebas E2E. Todo estaba en verde, así que abrí un pull request y fue aprobado. Pero después de hacer merge en develop, mi amigo Aitor descubrió que había roto la UI 🥲.
¿Por qué pasó esto?
Tenía pruebas para verificar mi código y estaba seguro de que los datos se renderizaban correctamente. Sin embargo, pasé por alto los componentes hijos y los estados de la UI como :hover o :focus, que dependen de la estructura del DOM y de las reglas de CSS.
En interfaces sensibles, los cambios estructurales pueden afectar bastante los estados visuales. Mis pruebas confirmaban que "los elementos se renderizan", pero olvidé la importancia del CSS y de los estados de diseño. ¿Cómo saber si los cambios en el HTML afectarán la UI?
💀 Es fundamental que las pruebas definan claramente su alcance.
Solo en la rama develop o si un compañero de equipo conoce bien la funcionalidad es posible notar esos detalles rotos. Esto es vital para la experiencia de usuario. Para evitar romper la UI y alertarme a mí o a un compañero sobre su importancia, podemos usar pruebas de snapshot con Jest (Jest Snapshot testing).
Como siempre, prefiero usar un escenario real para explicar y entender mejor la situación. Veamos el siguiente caso.
El escenario
Llevas más de cuatro meses sin tocar una aplicación, pero tomas un ticket para hacer un cambio pequeño. La aplicación muestra una lista de productos.

Mi prueba de productos con Testing Library
Tengo una plantilla HTML que muestra una lista de productos. La prueba usa data-testid para encontrar el título y asegurarse de que el elemento se haya renderizado en el componente.
<div class="products-list">
@for (p of products$ | async; track p) {
<div class="products-list__item">
<div class="products-list__item__title" data-testid="product-title">
<div>
<span>{{ p.title }}</span>
</div>
</div>
</div>
}
</div>
En mi archivo de prueba product.spec.ts, verifico que los productos se hayan renderizado:
it('should render products', async () => {
await setup();
const productsTitle = screen.getAllByTestId('product-title');
const productTitles = productsTitle.map((title) =>
title.textContent.trim(),
);
expect(productTitles).toEqual(fakeProducts.map((product) => product.title));
});Código fuente completo de
product.spec.ts
import { ProductsListComponent } from './products-list.component';
import { ProductModel } from '../../products/models/product.model';
import { ProductsService } from '../../products/services/products.service';
import { of } from 'rxjs';
import { render, screen } from '@testing-library/angular';
describe('ProductListComponent', () => {
const fakeProducts: ProductModel[] = [
// Productos simulados (mock)
];
const productsServiceMock = {
products$: of(fakeProducts),
};
it('should render products', async () => {
await setup();
const productsTitle = screen.getAllByTestId('product-title');
const productTitles = productsTitle.map((title) =>
title.textContent.trim(),
);
expect(productTitles).toEqual(fakeProducts.map((product) => product.title));
});
const setup = async () => {
await render(ProductsListComponent, {
providers: [{ provide: ProductsService, useValue: productsServiceMock }],
});
};
});
Al ejecutar la prueba, parece que todo funciona perfectamente ✅.

Algo me llamó la atención en la estructura, así que decidí eliminar algunos elementos div innecesarios en el HTML. Tras hacer los cambios, ejecuté mi prueba y todo se veía en verde. ✅🥳

Abro la aplicación y parece que todo funciona como se esperaba, con las pruebas en verde... ¿o no?

¿Notaste las diferencias?
Como llevaba meses sin tocar este código, no me di cuenta de que se eliminó el efecto "ellipsis" (los puntos suspensivos) en el texto. ¿Por qué mi prueba no me avisó de que había roto la UI?

La razón es simple: nuestra prueba está enfocada en comprobar que la aplicación funciona y renderiza los productos, pero no se preocupa por los cambios de estructura en la UI (como el foco de un elemento u otros estados visuales). Para evitar este tipo de problemas, aquí es donde entran las pruebas de snapshot con Jest.
Qué es Jest Snapshot
En un flujo de trabajo normal, cuando hacemos cambios en la UI y todo parece correcto, el proceso habitual suele ser abrir el navegador, cargar la aplicación completa y comparar la interfaz. Pero ¿qué pasa cuando se trata de un componente aislado, como una celda de tabla, un estado pequeño de una fila, un toggle o una tarjeta? Suena complicado, ¿verdad?
En estos casos es donde Jest Snapshot destaca. Nos ayuda a garantizar que la UI no cambie de forma inesperada al comparar la salida generada con un archivo de referencia guardado previamente. La prueba fallará cuando los dos snapshots no coincidan, ya sea porque ocurrió un cambio no deseado o porque el snapshot de referencia necesita actualizarse con la nueva versión.
Lee más sobre Snapshot Testing
Cuando escribimos una prueba con snapshot, el archivo resultante debe guardarse en el control de versiones junto con nuestro código. En la siguiente ejecución, Jest compara la salida renderizada con el snapshot previo: si nada ha cambiado, la prueba pasa; pero si alteramos algo, como quitar un div, la prueba falla y muestra las diferencias exactas.
Veamos cómo implementarlo en nuestro proyecto.
Implementando Jest Snapshot
Primero restauro el div en mi aplicación y aplico otro enfoque en mi prueba para tener total seguridad de no romper la UI. Necesito cambiar la función setup para que retorne el fixture, desde donde tomaré la referencia para mi snapshot:
const setup = async () => {
const { fixture } = await render(ProductsListComponent, {
providers: [{ provide: ProductsService, useValue: productsServiceMock }],
});
return {
fixture
};
};A continuación, agrego una nueva prueba para asegurarme de que los cambios de UI no rompan nada y todo se renderice bien. Tomo el fixture de la función setup y uso la aserción con toMatchSnapshot:
it('should display the correct list of product title', async () => {
const { fixture } = await setup();
expect(fixture).toMatchSnapshot();
});Ejecutamos la prueba y Jest creará el snapshot inicial con la primera versión del código:

Tras la ejecución, se crea automáticamente un directorio __snapshots__ junto a product-list.component.spec.ts:

Ahora, si otro compañero del equipo llega a trabajar en la aplicación y decide eliminar el div, veamos qué ocurre:

Exacto: nuestra prueba se queja de que rompimos el comportamiento esperado. Si alguien creó un snapshot, es porque esa parte de la estructura es relevante para el proyecto.
Esa es la clave. Si contamos con una prueba que protege el renderizado de la UI, es porque esa estructura importa y nos da confianza. En ese momento, cualquier compañero verá el diff y comprenderá de inmediato por qué falló la prueba.
¿Pero fallará con cada cambio que hagamos?
Debemos entender que Jest compara la salida generada contra el snapshot anterior. Por lo tanto, si agrego una nueva línea o un elemento, la prueba fallará. Eso es verdad. Pero un error común al principio es escribir pruebas que asumen más responsabilidades de las debidas. Por eso es muy importante escribir una prueba que se encargue exclusivamente de cuidar la salida estructural.
En mi código mantengo una primera prueba responsable de encontrar los elementos con los nombres de productos, mientras que otra prueba separada se encarga únicamente de la estructura de la lista.
Ahora bien, cuando hacemos cambios intencionales en la UI, las pruebas de snapshot van a fallar porque la salida ya no coincide con el archivo guardado. En esos casos necesitamos actualizar los snapshots.
Podemos revisar el diff para confirmar que los cambios son intencionales y que no introducen problemas visuales imprevistos. Al tener el snapshot, presto atención a los elementos, atributos y textos que se hayan añadido, eliminado o modificado.
Si todo se ve como esperamos, podemos actualizar los snapshots ejecutando Jest con la bandera -u:
npm test -u
Listo, tenemos nuestro snapshot actualizado y seguimos protegiendo nuestro diseño contra cambios accidentales. 🥳
No son perfectos 😭
Si has llegado hasta aquí es porque realmente te interesa aprender sobre snapshot testing. Durante el artículo hice hincapié en usar snapshots en puntos atómicos de tu aplicación. Mi ejemplo se enfocó en cambios de estructura y mencioné escenarios como una celda, una fila o piezas atómicas de la interfaz.
Una de las mayores desventajas de las pruebas de snapshot ocurre cuando se usan en componentes enormes donde hay cambios constantes. En esos casos, muchos desarrolladores terminan actualizando el snapshot automáticamente sin revisar el diff. Dejamos de prestarle atención porque hay demasiados cambios para revisar en un solo PR.
⚠️ Por eso te recomiendo leer sobre cómo escribir snapshots efectivos y las cosas que debes evitar con snapshots, un artículo de Kent C. Dodds.
Por favor, evita usar snapshots en componentes grandes o con demasiadas dependencias. Los snapshots funcionan mucho mejor para componentes simples con una responsabilidad clara y aislada. Por eso menciono ejemplos como <row/> o <app-icon/>.
Si tienes la libertad en tu proyecto, también te recomiendo explorar Cypress Component Testing, Cypress Visual Testing o herramientas modernas como Vitest junto a Playwright Component Testing.
Pruebas modernas 🤔 ¿Vitest? 🧪 ¿Playwright con Angular?
Si eres como yo y quieres aprender prácticas modernas de testing en Angular, incluyendo trabajar con Vitest, probar Observables, Forms y Router, evitar errores comunes con Mocks, Spies y Fakes (como me pasó a mí), y construir una estrategia sólida de pruebas (algo en lo que siempre busco mejorar), junto con el uso de Testing Library y Playwright Component Testing, todo en un solo lugar:
Te recomiendo echar un vistazo a “Pragmatic Angular Testing” de Younes Jaaidi. Younes comparte todos los desafíos, lecciones aprendidas y su experiencia real probando en Angular, todo en un curso muy completo.

Resumen
Después de probar las pruebas de snapshot, ahora veo lo útiles que son para asegurar que la apariencia de nuestra aplicación no se rompa por accidente. Ya usamos otras pruebas para verificar que las cosas funcionen bien, pero las pruebas de snapshot nos ayudan a comprobar que también se vean bien. Para explicarle las pruebas de snapshot a mi hijo Edgar, le dije: “Una prueba de snapshot es como tomarle una foto a tu UI”, recordando cómo se veía una parte de tu aplicación en un momento dado. Si después se ve distinta, la prueba te avisará.
Eso sí, ten presente que las pruebas de snapshot no reemplazan a tus otras pruebas. Son otra capa de seguridad visual enfocada en probar partes pequeñas, lo que hace que las pruebas sean más fáciles de entender y menos propensas a fallar por motivos equivocados. Cuando un snapshot falla, solo tengo que revisar qué cambió en esa 'foto' para confirmar si el cambio era intencional.
En general, considero que las pruebas de snapshot son muy útiles para partes de la aplicación donde la estructura y el aspecto visual son clave. Nos ayudan a detener problemas visuales inesperados antes de que lleguen a los usuarios. No son una solución mágica, pero ayudan bastante.
Ahora veo que las pruebas de snapshot son una excelente forma de mantener la UI en buen estado y evitar sorpresas desagradables 😊
¡Feliz testing!
Foto de Konstantin Dyadyun en Unsplash
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Preguntas Frecuentes
¿Qué es Jest snapshot testing y cómo ayuda a prevenir regresiones de UI?
Las pruebas de snapshot de Jest capturan la estructura HTML renderizada de un componente y la guardan en un archivo de snapshot. En las siguientes ejecuciones de pruebas, Jest compara el render actual con el snapshot guardado. Si algo en la estructura del DOM cambia, la prueba falla y te alerta sobre una posible regresión inesperada en la UI antes de que el cambio llegue a una revisión o a una rama compartida.
¿Cuándo debería usar pruebas de snapshot en lugar de pruebas basadas en aserciones tradicionales?
Las pruebas de snapshot son más útiles en componentes donde la estructura exacta del DOM importa para la apariencia visual, como cuando las reglas de CSS dependen del anidamiento de elementos o nombres de clases específicos. Úsalas junto a pruebas basadas en aserciones para cubrir tanto el renderizado de datos como la integridad estructural. Las pruebas normales verifican la lógica y los datos, mientras que los snapshots protegen la salida HTML general contra cambios inesperados.
¿Cómo actualizo un snapshot de Jest después de hacer un cambio de UI intencional?
Ejecuta Jest con la bandera --updateSnapshot o -u para regenerar los archivos de snapshot desactualizados. Jest sobrescribirá los snapshots anteriores con la nueva salida renderizada. Después de actualizar, revisa el diff con cuidado para confirmar que solo se capturaron los cambios esperados y luego haz commit de los archivos de snapshot actualizados junto con los cambios de código.
¿Pueden las pruebas de snapshot de Jest detectar errores de UI relacionados con CSS?
Los snapshots de Jest capturan la estructura HTML y las clases de tu componente, pero no renderizan CSS real. Pueden detectar cambios en nombres de clases o anidamiento del DOM que afectarían las reglas de CSS, lo cual suele ser suficiente para detectar regresiones estructurales. Para pruebas visuales a nivel de píxel necesitarías una herramienta dedicada de regresión visual, pero las pruebas de snapshot ofrecen una primera capa de protección rápida y ligera.
Artículos Relacionados
Cómo configurar Jest en Angular 18
Al escribir pruebas en Angular, Jasmine y Karma vienen configurados por defecto como librería y test runner, pero el equipo de Angular ha estado trabajando para traer Jest. Hoy vamos a experimentar con el soporte experimental de Jest Builder en Angular.
Cómo y cuándo usar Mocks vs Spies en pruebas unitarias (Vitest, Jest y Jasmine)
Aprende las diferencias prácticas entre Mocks y Spies en pruebas unitarias. Revisa ejemplos prácticos con Vitest moderno (vi.fn, vi.spyOn), Jest y Jasmine en Angular.
Cómo solucionar el error de Jest "Class constructor HTMLElement cannot be invoked without 'new'"
Uso Angular Testing Library para probar mis componentes y funciona de maravilla (me encanta). Sin embargo, me encontré con una situación particular al usar un Web Component dentro de mi componente, lo que provoca el error "Class constructor HTMLElement cannot be invoked without 'new'".
Cómo mockear dependencias con Jest
Cuando probamos nuestro código, muchas funciones tienen dependencias internas que no queremos invocar directamente. Aprende cómo mockear dependencias con Jest paso a paso.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.