Screenshot tests
Los screenshot tests validan la UI comparando imágenes.
Renderizas una vista → generas un snapshot → lo comparas con una imagen de referencia.
De este modo, si algo cambia → el test falla.
Sirven para detectar cambios visuales sin depender de la lógica.
¿Por qué deberías usarlos?
- Detectas regresiones visuales rápido
- Evitas bugs tontos en UI
- Revisas cambios sin abrir la app
- Integras en CI fácil
- Ganas confianza al refactorizar UI
Automatizas lo que normalmente revisas “a ojo”
Librerías más usadas en el desarrollo Android.
1. Paparazzi
- No hace falta emulador
- Va sobre JVM
- Muy rápido
Será el que utilicemos en este ejemplo. ¿Por qué Paparazzi?
- ⚡ Velocidad brutal (no requiere de emulador)
- 🤖 Perfecto para CI
- 🧪 Tests deterministas
- 🧩 Funciona genial con Views y Compose
La mejor relación simplicidad + velocidad ahora mismo
2. Shot (Karumi)
- Usa instrumentation tests
- Necesita emulador/dispositivo
3. Roborazzi
- Basado en Robolectric
- Alternativa moderna a Paparazzi
4. Compose Preview Screenshot Testing
- Integrado con Compose tooling
- Más limitado pero útil
Instalación de Paparazzi
Version Catalog (libs.versions.toml)
[versions]
paparazzi = "2.0.0-alpha04"
[libraries]
paparazzi = { group = "app.cash.paparazzi", name = "paparazzi", version.ref = "paparazzi" }
[plugins]
paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" }
build.gradle.kts (root)
plugins {
alias(libs.plugins.paparazzi) apply false
}
build.gradle.kts (module)
plugins {
alias(libs.plugins.paparazzi)
}
dependencies {
testImplementation(libs.paparazzi)
}
Crear tu primer screenshot test
Vamos a usar un sencillo composable como ejemplo:
@Composable
fun CustomButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
text: String,
buttonType: ButtonType = ButtonType.PRIMARY
) {
val primaryColors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
)
val secondaryColors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary,
)
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = if (buttonType == ButtonType.PRIMARY) primaryColors else secondaryColors
) {
Text(text = text)
}
}
enum class ButtonType {
PRIMARY,
SECONDARY,
}
Test básico
class CustomButtonScreenshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_6,
theme = "Theme.ScreenshotTestTheme",
)
@Test
fun captureCustomButtonScreenshot() {
paparazzi.snapshot {
ScreenshotTestTheme {
CustomButton(
text = "Custom Button",
onClick = {}
)
}
}
}
}
Generar y verificar snapshots
./gradlew recordPaparazziDebug
./gradlew verifyPaparazziDebug
Multimódulo
Puedes ejecutar solo un módulo, clave para no ralentizar CI:
./gradlew :feature:home:verifyPaparazziDebug
Screenshot tests parametrizados
Aquí empieza lo interesante.
Nos permiten cubrir múltiples estados visuales de forma escalable sin duplicar tests.
@RunWith(Parameterized::class)
class CustomButtonScreenshotTest(
private val buttonType: ButtonType,
private val isEnabled: Boolean,
) {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_6,
theme = "Theme.ScreenshotTestTheme",
)
companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): List<Array<Any>> = listOf(
arrayOf(ButtonType.PRIMARY, true),
arrayOf(ButtonType.PRIMARY, false),
arrayOf(ButtonType.SECONDARY, true),
arrayOf(ButtonType.SECONDARY, false),
)
}
@Test
fun captureCustomButtonScreenshot() {
paparazzi.snapshot {
ScreenshotTestTheme {
CustomButton(
buttonType = buttonType,
enabled = isEnabled,
text = "Custom Button",
onClick = {}
)
}
}
}
}
Centralizar configuración con un Rule
Evita repetir configuración.
En multimódulo, llévatelo a un módulo tipo:
:core:testing-ui
PaparazziRule
class PaparazziRule(
deviceConfig: DeviceConfig = DeviceConfig.PIXEL_6,
theme: String = "Theme.ScreenshotTestTheme",
renderingMode: SessionParams.RenderingMode = SessionParams.RenderingMode.SHRINK,
showSystemUi: Boolean = false,
maxPercentDifference: Double = 0.1,
) : TestRule {
private val paparazzi = Paparazzi(
deviceConfig = deviceConfig,
theme = theme,
renderingMode = renderingMode,
showSystemUi = showSystemUi,
maxPercentDifference = maxPercentDifference,
)
fun snapshot(name: String? = null, composable: @Composable () -> Unit) {
paparazzi.snapshot(name, composable = composable)
}
override fun apply(base: Statement, description: Description): Statement =
paparazzi.apply(base, description)
}
Su uso en un test
@get:Rule
val paparazzi = PaparazziRule()
Configuración importante
Esto marcará la diferencia en nuestros test:
renderingMode
- SHRINK → Componentes / pantallas dinámicas
- V_SCROLL → Contenido largo (listas, columnas)
- NORMAL → Tamaño fijo del device
showSystemUi
falsepor defecto
Evita ruido (status bar, nav bar)
maxPercentDifference
- Muy útil en CI
Tolera pequeñas diferencias entre máquinas
Golden Rules of Screenshot Testing
1. Testea estados, no pantallas bonitas
Error típico: solo capturar el “happy path”.
Haz esto en su lugar:
- enabled / disabled
- loading
- error
- empty
Testea lo que puede romperse, no lo que ya funciona.
2. Un test = una intención clara
Evita snapshots gigantes con mil cosas.
Mal:
- Pantalla completa con 10 componentes
Bien:
- Un componente o estado concreto
Si falla, sabes exactamente qué ha roto.
3. Nombres explícitos en snapshots
Si no nombras bien → no sabes qué estás viendo.
paparazzi.snapshot(name = "primary_enabled") { ... }
Esto te salva cuando tienes 50 imágenes.
4. Evita contenido dinámico
Esto rompe tests constantemente:
- Fechas
- Animaciones
- Random
- Imágenes remotas
Mockea o fija valores
val fixedDate = LocalDate.of(2024, 1, 1)
5. Controla el tema SIEMPRE
Nunca dependas del tema por defecto.
ScreenshotTestTheme {
Content()
}
Si cambia el theme global → no rompes todo.
6. Usa deviceConfig de forma consciente
No hace falta testear en 10 dispositivos, empieza con uno:
DeviceConfig.PIXEL_6
Escala solo si hace falta:
- tablet
- landscape
- dark mode
Menos ruido, más valor.
7. Evita tests frágiles (flaky)
Si tus tests fallan sin motivo → los dejas de usar.
Causas típicas:
- fuentes distintas en CI
- rendering distinto
- diferencias mínimas de píxeles
Solución:
maxPercentDifference = 0.1
Tolerancia controlada.
8. Aísla dependencias
No metas lógica real dentro del snapshot.
Mal:
- ViewModel real
- llamadas a red
Bien:
- estado mockeado
El snapshot test no es un integration test
9. Usa parametrización con cabeza
Muy potente, pero cuidado.
No hagas esto 20 combinaciones inútiles, en su lugar haz combinaciones que aporten valor visual
Menos casos, mejor cobertura.
10. Revisa snapshots como código
Esto es clave.
Un cambio en snapshot = cambio en UI
- Revísalo en PR
- No hagas “accept all” automático
Si no lo revisas, no sirve para nada.
Errores típicos (rápido) ❌
- Testear pantallas enteras sin criterio
- No fijar datos → tests inestables
- No usar nombres → caos total
- Aceptar snapshots sin revisar
- Meter lógica real
Regla mental para cerrar
Si el test falla, debes saber qué ha cambiado sin abrir la app
Si no → el test no está bien diseñado.

Comentarios