At Google I/O 2024, one of the announcements that caught my eye was support for Compose Preview Screenshot Testing using the Compose Preview Screenshot Testing tool. Even though this is still in an experimental state, I couldn’t wait to dive in and have a play with this!
My new book, CI/CD for Android using GitHub Actions is now available 🚀
Setting up Screenshot Testing
Before we can start with screenshot testing, we need to configure our project to access this functionality. We’ll start here by adding the required library and plugin declarations to our libs.versions.toml file. Here we are adding the androidx-compose-ui-tooling library, along with the compose.screenshot plugin – this is what will handle the generation and verification of our screenshots.
Note: you’ll need to at least be using version 8.5.0-beta of the android gradle plugin, and 1.9.20 for Kotlin
[versions]
agp = "8.5.0-beta01"
kotlin = "1.9.20"
screenshot = "0.0.1-alpha01"
[libraries]
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"}
[plugins]
...
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
We’ll then need to apply this plugin within the module build.gradle file where we want this plugin to be enabled.
plugins {
...
alias(libs.plugins.screenshot)
}
Next, we’ll need to enable the experimental screenshot testing feature within our gradle.properties file.
android.experimental.enableScreenshotTest=true
We’ll also need to apply this experimental flag at the module-level.
android {
experimentalProperties["android.experimental.enableScreenshotTest"] = true
}
It’s important to note that here, we must be using at least version 1.5.4 of the compose compiler to enable screenshot testing support.
android {
...
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
Finally, we’ll add the compose tooling package as a dependency for our screenshot tests using screenshotTestImplementation.
dependencies {
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
}
Now that we have our project configured for compose screenshot testing, we’re ready to get started writing some tests!
Creating a screenshot test
So that we can create a screenshot test, we’ll need to start by creating a composable. I’ll create a simple composable representing a TextButton. For examples sake I’m going to override the colors, as we’ll use this to break the tests later!
@Composable
fun ActionButton(modifier: Modifier = Modifier) {
TextButton(modifier = modifier, onClick = { }, colors = ButtonDefaults.textButtonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
)) {
Text(
text = "Sign Up"
)
}
}
For our composable, we’ll need to create a new screenshot test so that snapshots can be generated. Here we’ll need to create a new sourceset, screenshotTest and created a new test file.
Within this file we’ll add a new preview for our composable, composing the previously created ActionButton composable. If you already have a preview for the corresponding composable, you can simply paste this into your test file.
class ScreenshotTest {
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MaterialTheme {
ActionButton()
}
}
}
Generating Screenshots
Now that we have our screenshot test in place, we’ll want to run the gradle command so that the screenshot generated (this will be used later for comparison).
./gradlew updateDebugScreenshotTest
Once this has been run, we’ll notice that a screenshotTest directory has been created within the debug source set. Within this, they’ll be a nested reference directory that contains the generated png file.
If you open up this file you’ll notice an image representation of our composable, this will be the same as you should see in the generated preview within Android Studio.
Validating Screenshots
Now that our screenshots have been generated, we can validate our existing implementation against them. We’ll do this by running the following command:
./gradlew validateDebugScreenshotTest
We haven’t changed any of our composable implementation, so as expected, the screenshot validation provides us with a successful result.
So that we can see an example of this failing, let’s go ahead and tweak the styling of our composable. For examples sake, let’s image an engineer changes the containerColor of our TextButton.
@Composable
fun ActionButton(modifier: Modifier = Modifier) {
TextButton(modifier = modifier, onClick = { }, colors = ButtonDefaults.textButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondary
)) {
Text(
text = "Sign Up"
)
}
}
Intentional or not, if we run the same test command again we will see that the tests fail because the result of our test does not match the snapshots in our project. This failure alerts us that something visual has changed in our composable, drawing our attention to it so that we can avoid a UI regression.
When to run screenshot commands
From the above section you’ll notice that there were two commands we used – updateDebugScreenshotTest and validateDebugScreenshotTest. With these commands we need to make sure that our screenshots are kept up-to-date with the latest changes in our project, but we don’t want to be updating them all of the time – as we could accidentally update screenshots with UI regressions.
For validateDebugScreenshotTest, we’ll want to run this whenever code is being committed to the project – so ideally on pull requests, failing the request if the check fails.
When it comes to updateDebugScreenshotTest, we’ll only want to run this when there are intended changes made to our UI. Some examples of this could include:
- making a change to a component in our design system
- adding a new component to a pre-existing screen
- adding a new screen that we want to have screenshot tests for
With the examples above, we can see that we only want to run this update command when we are making intended changes to screens and/or components. It could also be the case that we have a pull request that makes intended and unintended changes – so it could be possible to accidentally update screenshots when it was not intended to.
To avoid any accident changes, updateDebugScreenshotTest should not be run automatically by CI and any screenshot changes in pull requests should be flagged be automation so that changes can be checked by reviewers.
If you’re looking to learn more about how to automate these kinds of processes in your Android projects, check out my new book.
And that’s it! As we can see, adding screenshot tests for our composables involves very little effort. With these tests we can reduce the number of UI regressions occurring our project, helping us to create a consistent design experience in our app.
I’ll be taking a look at some more things from Google I/O in some following posts, so stated tuned for more Android!