Skip to content

AckeeCZ/ackee-android-snapshots

Repository files navigation

ackee|Ackee Android Snapshots

Ackee Android Snapshots

License Maven Central

Overview

Ackee Android Snapshots is an opinionated framework designed for snapshot testing in Android projects, particularly focusing on Jetpack Compose UI components. It leverages Paparazzi for rendering snapshots, Showkase for component discovery, and Kotest as the testing framework.

Architecture

The framework is designed with extensibility in mind, using a modular architecture:

  • annotations module contains shared code for preview annotations
  • framework module defines the base interfaces (SnapshotEngine, SnapshotStrategy) and common testing infrastructure
  • paparazzi module provides a concrete implementation of the snapshot engine using Paparazzi
  • Additional snapshot engine implementations can be added by implementing the SnapshotEngine interface

Features

  • Automated snapshot testing for Jetpack Compose components
  • Support for different device configurations (Phone/Tablet)
  • Support for different device orientations (Portrait/Landscape)
  • Dark/Light theme testing
  • Font scale testing
  • Component and full-screen testing strategies
  • Integration with popular testing tools:
    • Paparazzi for snapshot generation
    • Showkase for component discovery
    • Kotest for test execution

Installation

Add the following configuration, depending on what you need. You should always use BOM to be sure to get binary compatible dependencies. If you have a custom snapshots engine, you will need only annotations and framework dependencies. Additionally, you can add paparazzi as well for our Paparazzi engine implementation.

Annotations & Framework

For annotations and framework artifacts you will need the following configuration:

[versions]
ackee-snapshots-bom = "SPECIFY_VERSION"
showkase = "SPECIFY_VERSION"

[dependencies]
ackee-snapshots-bom = { group = "io.github.ackeecz", name = "snapshots-bom", version.ref = "ackee-snapshots-bom" }
ackee-snapshots-annotations = { group = "io.github.ackeecz", name = "snapshots-annotations" }
ackee-snapshots-framework = { group = "io.github.ackeecz", name = "snapshots-framework" }

# Showcase is needed for annotating previews and passing a selected PreviewSnapshotStrategy
showkase-core = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase-processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }

and specify dependencies

implementation(platform(libs.ackee.snapshots.bom))
implementation(libs.ackee.snapshots.annotations)
testImplementation(libs.ackee.snapshots.framework)

implementation(libs.showkase.core)
ksp(libs.showkase.processor)

Paparazzi

If you use paparazzi artifact, you will need the following additional configuration:

[versions]
# Ensure that this version is equal or greater than Paparazzi version in this project. Otherwise
# there might be incompatibility between Paparazzi runtime used in Ackee Snapshots and Paparazzi
# Gradle plugin used by your app.
paparazzi = "SPECIFY_VERSION"

[dependencies]
ackee-snapshots-paparazzi = { group = "io.github.ackeecz", name = "snapshots-paparazzi" }

[plugins]
paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" }

Apply the Paparazzi Gradle plugin in your module's build.gradle.kts:

plugins {
    id(libs.plugins.paparazzi)
}

and specify dependencies

testImplementation(libs.ackee.snapshots.paparazzi)

Configuration

The library supports two main testing strategies:

  1. Component Testing (SnapshotStrategy.Component):

    • Components are rendered in the smallest possible size
    • Snapshots are not taken for different devices
    • Ideal for testing individual UI components
  2. Screen Testing (SnapshotStrategy.Screen):

    • Screens are rendered within the full frame of the selected device configuration
    • Device configuration includes both device type and orientation
    • Snapshots are taken for all configured device configurations
    • Perfect for testing complete screens and layouts within a device frame

To determine on a Preview level which strategy to use the predefined annotation metadata tag can be used along the ShowkaseComposable annotation. Showkase collects both @Preview and @ShowkaseComposable methods, but only the latter can be used to specify the strategy. In the following example @Preview is used to render the component in Android Studio and @ShowkaseComposable for the snapshots.

@Preview
@ShowkaseComposable(extraMetadata = [PreviewSnapshotStrategy.Component])
@Composable
fun PrimaryButtonPreview() {
    SnapshotsSampleTheme {
        PrimaryButton(text = "Click me") { }
    }
}

Limits

Since paparazzi is a JUnit4 rule implementation, it is not possible within the single test case to run multiple snapshots with different device specifications hence only a single device may be used for a single test case. To take snapshots on multiple devices specify a separate test case for each device. Check the sample below.

Usage

  1. Create snapshot test classes for different testing strategies:
val fontScales = listOf(FontScale.NORMAL, FontScale.LARGE)
val defaultFontScale = listOf(FontScale.NORMAL)

val before = { _: Context ->
    // nothing
}

val theme: @Composable (UiTheme, @Composable () -> Unit) -> Unit = { uiTheme, content ->
    SnapshotsSampleTheme(
        darkTheme = uiTheme == UiTheme.DARK,
        dynamicColor = false
    ) {
        content()
    }
}

val screenPreviews = Showkase.getMetadata().componentList.filter {
    !it.extraMetadata.contains(PreviewSnapshotStrategy.Component)
}

val componentPreviews = Showkase.getMetadata().componentList.filter {
    it.extraMetadata.contains(PreviewSnapshotStrategy.Component)
}

class LightComponentsSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = fontScales,
    showkasePreviews = componentPreviews,
    theme = theme,
    uiTheme = UiTheme.LIGHT,
    strategy = SnapshotStrategy.Component
)

class DarkComponentsSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = defaultFontScale,
    showkasePreviews = componentPreviews,
    theme = theme,
    uiTheme = UiTheme.DARK,
    strategy = SnapshotStrategy.Component
)

class LightPortraitPhoneSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = fontScales,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.LIGHT,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.PIXEL_6,
            orientation = DeviceOrientation.PORTRAIT
        )
    ),
)

class DarkPortraitPhoneSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = defaultFontScale,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.DARK,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.PIXEL_6,
            orientation = DeviceOrientation.PORTRAIT
        )
    ),
)

class LightLandscapePhoneSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = fontScales,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.LIGHT,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.PIXEL_6,
            orientation = DeviceOrientation.LANDSCAPE
        )
    ),
)

class DarkLandscapePhoneSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = defaultFontScale,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.DARK,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.PIXEL_6,
            orientation = DeviceOrientation.LANDSCAPE
        )
    ),
)

class LightTabletSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = fontScales,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.LIGHT,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.NEXUS_10,
            orientation = DeviceOrientation.LANDSCAPE
        )
    ),
)

class DarkTabletSnapshotTests : PaparazziSnapshotTests(
    before = before,
    fontScales = defaultFontScale,
    showkasePreviews = screenPreviews,
    theme = theme,
    uiTheme = UiTheme.DARK,
    strategy = SnapshotStrategy.Screen(
        DeviceConfig(
            device = Device.NEXUS_10,
            orientation = DeviceOrientation.LANDSCAPE
        )
    ),
)
  1. Annotate your composables with Showkase and specify the strategy:
@Preview
@ShowkaseComposable(
    extraMetadata = [PreviewSnapshotStrategy.Component] // For component testing
)
@Composable
fun ButtonPreview() {
    AppTheme {
        Button(onClick = {}) {
            Text("Click me")
        }
    }
}

@Preview
@ShowkaseComposable(
    extraMetadata = [PreviewSnapshotStrategy.Screen] // For screen testing
)
@Composable
fun ScreenPreview() {
    AppTheme {
        HomeScreen()
    }
}

This setup will:

  • Test UI components in their minimal size with both light/dark themes and font scales
  • Test full screens on a phone (Pixel 6) with both themes and font scales
  • Test full screens on a tablet (Nexus 10) with both themes and font scales

ℹ️ Note on Font Scale Configuration: The example shows fontScales (containing both NORMAL and LARGE) for light theme tests, and defaultFontScale (containing only NORMAL) for dark theme tests. This pattern helps avoid redundant snapshot generation - since you're already testing font scale variations with the light theme, using only the normal font scale for dark theme tests prevents creating duplicate snapshots while still providing dark theme coverage.

Project Structure

  • bom: BOM module
  • annotations: Shared code for preview annotations. Used in test and production source sets.
  • framework: Core framework and testing infrastructure
  • paparazzi: Paparazzi integration for snapshot generation
  • sample: Example application demonstrating usage

Working with Snapshots

Generating Screenshots

To generate screenshots, run the Paparazzi record task:

./gradlew cleanRecordPaparazziDebug

This will generate screenshots for all annotated composables in your source set. The snapshots are stored in:

{module}/src/test/snapshots

Verifying Screenshots

To verify that your UI components haven't changed unexpectedly, run:

./gradlew verifyPaparazziDebug

If there are any differences between the recorded and current snapshots, the test will fail and Paparazzi will generate a report showing the differences.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Credits

Developed by Ackee team with 💙.

About

Framework for a snapshot testing used in Ackee projects

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 5

Languages