Menu Button
Go to Top

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

  • false por 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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comentarios

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos. Contiene enlaces a sitios web de terceros con políticas de privacidad ajenas que podrás aceptar o no cuando accedas a ellos. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad