Screenshot testing: capturar cada estado y cazar regresiones visuales
Hay una clase entera de defectos que ninguna aserción textual puede describir: el contraste que se pierde, el texto que se corta, el margen que colapsa, el modo oscuro que nadie revisó. La comprobación por captura de imagen ataca ese hueco convirtiendo cada estado del modelo MVI en una imagen de referencia que el sistema compara en cada integración. Esta lección explica qué prueba realmente una captura, cómo el catálogo de estados se transforma en catálogo de imágenes casi sin esfuerzo, qué exige el determinismo del renderizado, y por qué la aprobación de una referencia nueva es una decisión humana que no se debe automatizar.
Toda la batería de pruebas construida hasta aquí comparte una limitación que no se resuelve escribiendo más aserciones: solo puede afirmar aquello que alguien supo poner en palabras de antemano. Un test comprueba que el botón existe, que está habilitado y que dice reintentar; no comprueba que se vea, porque nadie escribió una aserción sobre el contraste, sobre el recorte del texto a doscientos por ciento de escala tipográfica o sobre el margen que desapareció al migrar de versión. La comprobación por captura invierte esa lógica: en lugar de enumerar lo que debe cumplirse, congela una imagen que el equipo aceptó como correcta y avisa de cualquier diferencia respecto de ella. Es la única técnica de la pirámide cuya afirmación no hay que redactar, y por eso es la única que detecta regresiones que nadie anticipó. En una arquitectura MVI el ajuste es casi perfecto, porque la pantalla ya es una función pura de un estado enumerable, y una función pura de un tipo cerrado se puede fotografiar entera.
- Delimitar qué clase de defectos solo detecta una comparación de imágenes.
- Derivar el catálogo de capturas directamente del catálogo de estados del modelo.
- Garantizar el determinismo del renderizado para evitar diferencias que no son regresiones.
- Tratar la aprobación de referencias como decisión humana con trazabilidad en el control de versiones.
Lo que solo ve una imagen
Conviene ser preciso sobre el hueco que se está cubriendo, porque la técnica se defiende sola si se enuncia bien y se vuelve un capricho si se justifica mal.
Recorte y desbordamiento
Un nombre largo, una traducción al alemán o una escala tipográfica grande cortan un texto que la semántica sigue afirmando presente.
Tema y contraste
El modo oscuro que nadie abrió, el color de marca sobre fondo claro, el estado deshabilitado que quedó ilegible.
Espaciado y alineación
Márgenes que colapsan tras actualizar una librería de componentes, alturas que cambian sin que ninguna aserción lo note.
Composición completa
El elemento que existe en el árbol pero queda tapado, desplazado fuera del área visible o solapado con otro.
La forma más honesta de justificarlo es reconocer la asimetría respecto de la lección anterior. Un test de semántica afirma que la pantalla significa lo correcto; una captura afirma que la pantalla se ve como se acordó. Ninguna de las dos implica a la otra: una interfaz puede tener semántica impecable y presentación rota, o pixel perfecto y ser inaccesible. Por eso la captura no sustituye a nada, añade una dimensión.
También hay que ser claro con lo que no aporta. Una captura no valida que el diseño sea bueno, ni que la interacción tenga sentido, ni detecta nada la primera vez que se genera: en el momento de crear la referencia, la imagen es correcta por definición. Su valor aparece en la segunda ejecución y crece con el tiempo, lo cual la convierte en una inversión con retorno diferido, y eso conviene decirlo antes de venderla al equipo.
El catálogo de estados es el catálogo de imágenes
Aquí MVI paga un dividendo inesperado. Como el estado es un tipo cerrado y la pantalla es una función de él, el conjunto de capturas relevantes no hay que inventarlo: ya está escrito en el modelo.
private val estadosDeReferencia = listOf(
"vacio" to CarritoState(),
"cargando" to CarritoState(cargando = true),
"error" to CarritoState(error = "Sin conexion"),
"una-linea" to CarritoState(lineas = listOf(lineaCorta())),
"nombre-largo" to CarritoState(lineas = listOf(lineaConNombreDe120Caracteres())),
)
@Test
fun `capturas del carrito en tema claro y oscuro`() {
estadosDeReferencia.forEach { (nombre, estado) ->
listOf(false, true).forEach { oscuro ->
capturar(nombre = "carrito-$nombre-${if (oscuro) "oscuro" else "claro"}") {
TemaApp(oscuro = oscuro) {
CarritoPantalla(estado = estado, alEnviar = {})
}
}
}
}
}
De cinco estados y dos temas salen diez imágenes con una sola función de test, y añadir un estado nuevo al modelo solo requiere una línea en la lista. Esa proporción es la razón por la que la técnica encaja tan bien aquí y tan mal en arquitecturas donde alcanzar el estado de error exige simular una caída de red.
El mismo mecanismo se extiende a las dimensiones que suelen olvidarse: escala tipográfica grande, ancho de tableta, idioma con palabras largas. Cada una multiplica el catálogo, de modo que conviene elegir con criterio en lugar de generar el producto cartesiano completo; treinta imágenes revisables valen más que trescientas que nadie mira.
Hay además una decisión de granularidad que condiciona todo lo demás: capturar pantallas completas o capturar componentes. La pantalla completa detecta problemas de composición global —solapamientos, desbordamientos, jerarquía visual— pero produce imágenes que cambian ante cualquier retoque de cualquier parte, de modo que una modificación en la cabecera invalida las diez referencias de la pantalla. El componente aislado produce diferencias muy localizadas y fáciles de aprobar, a cambio de no ver nunca cómo conviven las piezas. La combinación que funciona en la mayoría de equipos es capturar el sistema de componentes con granularidad fina y las pantallas solo en sus estados canónicos.
El nombre del archivo es lo único que verá quien revise una diferencia dentro de seis meses. Una convención que codifique pantalla, estado, tema y variante en ese orden permite ordenar el directorio alfabéticamente y leerlo como un índice del catálogo. Con nombres correlativos o generados por posición, el mismo directorio es una carpeta de imágenes indistinguibles y la revisión se vuelve un ejercicio de adivinación.
flowchart LR M[Tipo cerrado del estado] --> L[Lista de estados de referencia] L --> R[Renderizado determinista] R --> N[Imagenes nuevas] N --> C[Comparacion pixel a pixel] B[Imagenes de referencia en el repositorio] --> C C --> D[Informe de diferencias] style B fill:#a6e3a1,color:#11111b style D fill:#f9e2af,color:#11111b
Determinismo o ruido
La única razón por la que esta técnica fracasa en la práctica es el falso positivo: diferencias de imagen que no corresponden a ninguna regresión. Un equipo tolera dos semanas de diferencias espurias y a la tercera desactiva la comprobación. Por eso el determinismo del renderizado no es un detalle de configuración sino la condición de supervivencia de la técnica.
Las fuentes de indeterminismo son pocas y conocidas. Las animaciones, si la captura ocurre en un fotograma arbitrario. El tiempo y el azar, si la pantalla muestra una fecha relativa o un identificador generado. La carga de imágenes remotas, que hay que sustituir por recursos locales fijos. Y, la más traicionera, el propio motor de dibujo: la misma pantalla renderizada por dos versiones de la máquina virtual o por dos sistemas operativos produce diferencias de suavizado imperceptibles para el ojo y evidentes para una comparación estricta.
La disciplina consiste en neutralizarlas todas en un único punto de preparación, para que ninguna captura dependa de que alguien se acordara.
@Before
fun fijarElEntorno() {
reloj.fijar(instanteFijo = "2026-01-01T09:00:00Z")
generadorDeIdentificadores.fijarSemilla(semilla = 42)
cargadorDeImagenes.servirSoloRecursosLocales()
animaciones.desactivar()
densidadYEscalaTipografica.fijar(densidad = 2.0f, escala = 1.0f)
}
Fíjate en que casi todas esas líneas son inyecciones de dependencia disfrazadas. Una pantalla que puede fotografiarse de forma determinista es, necesariamente, una pantalla que no lee el reloj del sistema ni genera azar por su cuenta, es decir, la misma disciplina que exigía el reducer puro reaparece aquí por un camino distinto. Cuando alguna de esas líneas resulta imposible de escribir porque el valor se obtiene en el interior de un composable, la captura acaba de señalar un acoplamiento que ninguna otra prueba había detectado.
Si cada persona regenera las imágenes en su máquina, el repositorio acumulará diferencias de suavizado que nadie sabrá interpretar y la comprobación morirá por desgaste. Las referencias deben producirse siempre en el mismo entorno, idealmente el de la integración continua o un contenedor idéntico a él, y la orden de regenerar debe ser un objetivo del sistema de construcción y no un gesto manual.
El umbral de tolerancia merece un comentario aparte, porque es donde se cometen los dos errores opuestos. Con tolerancia cero, cualquier variación del motor de dibujo rompe la integración. Con tolerancia generosa, un cambio real de color deja de detectarse. La política razonable consiste en fijar un umbral pequeño sobre el porcentaje de píxeles distintos y, sobre todo, en resolver el indeterminismo en su origen en lugar de compensarlo subiendo el número.
Aprobar es decidir
El punto que más se descuida es el flujo de trabajo, no la herramienta. Cuando una comparación falla, hay exactamente dos interpretaciones posibles: se ha roto algo o se ha cambiado algo a propósito. La máquina no puede distinguirlas, y cualquier intento de automatizar esa distinción destruye el valor de la técnica.
De ahí se sigue una regla práctica: las imágenes de referencia viven en el control de versiones y su actualización viaja en el mismo cambio que la modificación de interfaz que la justifica. Quien revisa ese cambio ve la imagen anterior y la nueva una junto a otra, y aprueba o rechaza mirando. Ese momento —una persona comparando dos imágenes antes de integrar— es donde reside todo el valor; el resto es infraestructura para llegar hasta él.
Esa regla tiene una implicación desagradable que conviene anticipar: el repositorio engorda. Cada estado, cada tema y cada configuración añade un archivo binario que el control de versiones no sabe comprimir entre revisiones, y un catálogo generoso puede sumar cientos de megabytes al cabo de un año. Las salidas razonables son dos: mantener el catálogo deliberadamente pequeño y significativo, o alojar las imágenes fuera del repositorio principal con un mecanismo de referencia por huella. Lo que no funciona es ignorar el problema hasta que clonar el proyecto tarde diez minutos.
También conviene decidir por adelantado qué ocurre cuando la comparación falla en una rama de trabajo compartida. La política sana es que la integración se detenga y el informe quede adjunto, nunca que las referencias se regeneren de forma automática al fallar. Un sistema que actualiza solo las imágenes cuando no coinciden es un sistema que siempre está verde y que nunca ha comprobado nada, y su existencia es peor que la ausencia total de capturas porque además genera confianza injustificada.
El informe de diferencias es también el mejor canal que existe entre desarrollo y diseño. Adjuntar las imágenes al cambio permite que quien diseñó la pantalla confirme que la desviación es la que pidió, y convierte una discusión abstracta sobre espaciados en una comparación concreta. Muchos equipos descubren que el retorno principal de estas pruebas no fue cazar regresiones sino acortar ese ciclo de revisión.
Merece la pena entender por qué esta técnica pertenece a una categoría distinta del resto de la pirámide. Todos los demás tests son deductivos: alguien formula una propiedad y la máquina comprueba si se cumple, de modo que el alcance de la batería queda acotado de antemano por la imaginación de quien la escribió. Ningún test convencional puede encontrar un defecto que su autor no supo anticipar, y por eso las regresiones que llegan a producción son casi siempre de la clase que nadie pensó en describir. La comparación de imágenes rompe esa limitación porque no comprueba una propiedad sino una identidad: no afirma que el margen deba medir dieciséis, afirma que hoy debe verse igual que ayer salvo que alguien decida lo contrario. Es una especificación por extensión en lugar de por comprensión, y su cobertura es, en el plano visual, total. Ese poder tiene un precio exactamente simétrico, y conviene aceptarlo con los ojos abiertos: al no saber qué es importante, señala cualquier diferencia con la misma urgencia, y por tanto traslada al ser humano toda la carga de decidir si lo que cambió importa. Esa es la razón por la que estas pruebas fracasan por motivos sociales antes que técnicos. No mueren porque la comparación sea difícil sino porque el equipo deja de mirar los informes, y deja de mirarlos en cuanto el ruido supera cierto umbral de paciencia. De ahí la conclusión que ordena todas las decisiones prácticas de esta lección: el objetivo de ingeniería no es capturar más pantallas ni afinar más el algoritmo de comparación, es proteger la credibilidad del informe. Una batería de treinta imágenes deterministas que el equipo revisa con atención vale infinitamente más que trescientas que fallan a diario y que todo el mundo aprueba en bloque sin abrirlas, porque esta última no es una red de seguridad: es un ritual que consume tiempo de máquina y produce la ilusión de estar vigilado.
Una última consideración sobre dónde colocar estas pruebas en el ciclo. Como son las más lentas y las que más dependen del entorno, su sitio natural no es el guardado del archivo sino la integración del cambio, donde el coste se paga una vez y la revisión ocurre de todos modos. Ejecutarlas en cada compilación local suele producir el efecto contrario al buscado: la gente aprende a saltárselas.
- Enumera los estados que tu tipo de estado puede representar y escribe la lista de estados de referencia con un nombre legible para cada uno.
- Genera las capturas en tema claro y oscuro y comprueba cuántos problemas visuales aparecen ya en la primera generación.
- Añade una variante con escala tipográfica grande y otra con un texto muy largo, y documenta lo que se rompe.
- Introduce a propósito un cambio de espaciado de cuatro unidades y verifica que la comparación lo detecta con el umbral que has fijado.
- Define por escrito quién aprueba una referencia nueva y en qué momento del flujo de revisión ocurre esa aprobación.