¿Qué es el snapshot testing de UI, y dónde realmente vale la pena en mobile?
Los tests de snapshot vienen en dos sabores muy distintos, estructural y de píxeles, y los dos son fáciles de sobreusar hasta volverse un sello que nadie lee.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es el snapshot testing de UI, y dónde realmente vale la pena en mobile?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.El snapshot testing es esa categoría de test que la gente sobreusa hasta que pierde sentido, o descarta como inútil — generalmente porque solo la vio en una de sus dos formas, que son muy distintas entre sí. Vale la pena separar esas formas antes de hablar de dónde cada una se gana el lugar.
#Dos cosas se esconden bajo un solo nombre
La primera forma, popularizada por Jest para React (y portada a React Native), es un snapshot estructural (serialization snapshot): renderizás el árbol de componentes, lo serializás a una representación de texto legible, y comparás ese texto contra un archivo commiteado en el repo. No se renderiza nada a píxeles; estás comparando una descripción estructurada de la salida — props, contenido de texto, nombres de elementos anidados.
La segunda forma es un snapshot visual/de píxeles: renderizás de verdad la UI — una jerarquía de vistas nativa, un árbol de Compose, una pantalla entera — a un bitmap, y comparás ese bitmap contra una imagen de referencia. Esto es lo que hacen herramientas como Paparazzi (Android, Compose y Views, sin necesidad de emulador), Roborazzi (basado en Robolectric), ios-snapshot-test-case de Facebook, o la librería swift-snapshot-testing de Point-Free. La comparación es genuinamente visual: el renderizado de fuentes, el espaciado, los colores, incluso diferencias de anti-aliasing a nivel sub-píxel pueden hacer fallar un diff.
Las dos comparan "lo que obtuvimos" contra "lo que aprobamos antes", y las dos fallan de la misma manera cuando algo cambió — la pregunta que tiene que responder quien revisa no es "¿esto pasa?", es "¿este cambio fue intencional?". Esa disciplina de revisión es toda la propuesta de valor, y también es exactamente donde los equipos se equivocan.
#Dónde realmente vale la pena
El snapshot testing se gana el lugar en unidades chicas, deterministas y de solo presentación — el tipo de componentes que exporta un design system: un Button, un Badge, una Card, un ícono en tres tamaños y dos temas. Son baratos de snapshotear porque sus entradas están totalmente controladas (pasás props literales, sin datos en vivo, sin timestamps), su espacio de salidas es lo bastante chico como para que una persona realmente pueda mirar cada imagen de referencia en una revisión de PR, y las regresiones en ellos son exactamente el tipo de cosa fácil de introducir sin querer y fácil de pasar por alto leyendo un diff del código fuente (un token compartido de Theme cambia y ahora cuarenta pantallas tienen 1px distinto de padding — eso nunca lo detectás solo con code review).
También vale la pena como sustituto rápido y determinista de una granja de dispositivos durante CI. Paparazzi es el ejemplo más claro: renderiza UI de Compose usando layoutlib de Android directamente sobre la JVM, sin necesidad de emulador ni dispositivo físico, así que una suite de screenshots que tardaría minutos en Firebase Test Lab corre en segundos en local y en cada PR. Esa velocidad es lo que hace viable el testing de regresión visual como un gate, no solo como un job nocturno.
#Dónde deja de valer la pena
El modo de fallo es conocido: una pantalla con contenido en vivo y no determinista (timestamps, ads, datos personalizados, animación a mitad de frame) produce un snapshot flaky por construcción, y los tests flaky se silencian, lo que borra silenciosamente la cobertura. El otro modo de fallo es social, no técnico: en cuanto "correr update-snapshots" se vuelve un acto reflejo, un test de snapshot deja de ser una pregunta ("¿quisiste cambiar esto?") y pasa a ser un checkbox que nadie lee de verdad — en ese punto está dando la sensación de cobertura sin la sustancia. Las pantallas profundas y que cambian seguido (una lista de settings a mitad de un rediseño, una pantalla de chat) también encajan mal — cada cambio de copy legítimo produce una imagen de referencia nueva para revisar, así que la relación señal-ruido de los diffs se derrumba.
La regla práctica que aplican los ingenieros senior: snapshotear las hojas (componentes individuales, primitivas del design system, composables chicos), no el árbol entero; mantener pocas imágenes de referencia y revisarlas como código, no aprobarlas de forma automática; y combinar los tests de snapshot con tests de comportamiento reales (¿tocar el botón llama al handler?) en vez de tratar un snapshot que pasa como prueba de que la función funciona — un snapshot solo prueba que los píxeles o la estructura coinciden con un estado anterior, no que ese estado anterior era correcto.
// React Native — snapshot estructural con Jest y @testing-library/react-nativeimport { render } from "@testing-library/react-native";import { PriceBadge } from "./PriceBadge";describe("PriceBadge", () => {it("renderiza un precio con descuento de forma consistente", () => {const { toJSON } = render(<PriceBadge amountCents={1999} originalCents={2999} currency="USD" />);expect(toJSON()).toMatchSnapshot();});it("sigue llamando a onPress cuando se toca la cinta de descuento", () => {const onPress = jest.fn();const { getByTestId } = render(<PriceBadge amountCents={1999} originalCents={2999} currency="USD" onPress={onPress} />);getByTestId("price-badge").props.onPress();expect(onPress).toHaveBeenCalledTimes(1);});});
// Android — snapshot de píxeles con Paparazzi para un componente de Compose, sin emuladorclass PriceBadgeSnapshotTest {@get:Ruleval paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_6,theme = "android:Theme.Material.Light.NoActionBar")@Testfun `badge de precio con descuento, tema claro`() {paparazzi.snapshot {AppTheme(darkTheme = false) {PriceBadge(amountCents = 1999, originalCents = 2999, currency = "USD")}}}@Testfun `badge de precio con descuento, tema oscuro`() {paparazzi.snapshot {AppTheme(darkTheme = true) {PriceBadge(amountCents = 1999, originalCents = 2999, currency = "USD")}}}}