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.
The framework is designed with extensibility in mind, using a modular architecture:
annotationsmodule contains shared code for preview annotationsframeworkmodule defines the base interfaces (SnapshotEngine,SnapshotStrategy) and common testing infrastructurepaparazzimodule provides a concrete implementation of the snapshot engine using Paparazzi- Additional snapshot engine implementations can be added by implementing the
SnapshotEngineinterface
- 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
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.
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)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)The library supports two main testing strategies:
-
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
-
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") { }
}
}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.
- 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
)
),
)- 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.
- 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
To generate screenshots, run the Paparazzi record task:
./gradlew cleanRecordPaparazziDebugThis will generate screenshots for all annotated composables in your source set. The snapshots are stored in:
{module}/src/test/snapshots
To verify that your UI components haven't changed unexpectedly, run:
./gradlew verifyPaparazziDebugIf there are any differences between the recorded and current snapshots, the test will fail and Paparazzi will generate a report showing the differences.
Contributions are welcome! Please feel free to submit a Pull Request.
Developed by Ackee team with 💙.
