Snapshot Testing Steps on Android
How to make sure that there are no bugs in your UI after new developments and how to create snapshot tests in different scenarios. I will explain step by step these questions.
In my previous article, I mentioned about the benefits of snapshot testing, why we should write it, and about the libraries used. In this article, we will write sample snapshot tests.
1. Record
We will create our snapshots using the paparazzi library. Paparazzi library allows us to produce snapshots without emulator or any device dependency. The example below shows how to create a snapshot of a compose component.
@get:Rule
val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_6)
@Test
fun filterSelectionComponentTest() {
paparazzi.snapshot {
FilterSelectionComponent()
}
}
@Composable
fun FilterSelectionComponent() {
..
}
We use the recordPaparazziDebug command to start the snapshot test and record snapshot.
./gradlew sample:recordPaparazziDebug
After successfully running our tests, we can see the snapshot of our component under the snapshot folder.
We can create a snapshot test like this for every component. Imagine you are developing a price range component like the one above. The snapshot you created is now in your project, so your teammates can visually follow this component you developed while opening merge request it will be much easier to follow the developments in the component when there are improvements that affect this component later.
It creates a snapshot of our component, but we need to check how it behaves in different scenarios. In this case, we will use TestParameterInjector to generate snapshots in different scenarios for each of our snapshots.
Generating snapshots in different scenarios
We have said that we can produce snapshots in different scenarios with TestParameterInjector , this is why we want the views we created to be displayed to the user without any UI problems.
To run snapshot tests according to different device configs
enum class DeviceConfigParameter(
val deviceConfig: DeviceConfig
) {
PIXEL_4(deviceConfig = DeviceConfig.PIXEL_4),
PIXEL_4_LAND(deviceConfig = DeviceConfig.PIXEL_4.copy(
orientation = ScreenOrientation.LANDSCAPE
)),
PIXEL_4_XL(deviceConfig = DeviceConfig.PIXEL_4_XL),
}
Here we define the devices that we want to test as enum. Paparazzi provides the device config.
@RunWith(TestParameterInjector::class)
class TestParameterInjectorTest(
@TestParameter config: DeviceConfigParameter
) {
@get:Rule
val paparazzi = Paparazzi(deviceConfig = config.deviceConfig)
...
}
I’m using DeviceConfigParameter, which I created with the @TestParameter annotation as a parameter to our test class. Then I use my device config when I create my Paparazzi object. So my Paparazzi object will be created as parameterized.
When I create the existing snapshots parameterized as above, the output will be like this.
We created our snapshots according to different devices, now let’s test how our components output in different inputs. As input, I will create test parameters and my component will use these parameters. I can take an option list as input for the price range component or use a different title.
object FilterItemsProvider : TestParameter.TestParameterValuesProvider {
override fun provideValues(): List<FilterItems> =
listOf(
FilterItems(
filterItems = listOf(
FilterItem("$0 - $50", true),
FilterItem("$50 - $75", true),
FilterItem("$75 - $100", true),
)
),
FilterItems(
filterItems = listOf(
FilterItem("$100 - $150", false),
FilterItem("$200 - $250", false),
FilterItem("$300 - $350", false),
)
)
)
}
I created the cases that I want to test in TestParameterValuesProvider.
@Test
fun filterSelectionView(@TestParameter(valuesProvider = FilterItemsProvider::class) filterProvider: FilterItems) {
val title = "Price range"
paparazzi.snapshot {
FilterSelectionView(
title = title,
filters = filterProvider.filterItems
)
}
}
While creating my test method, I use my provider that I created for my test cases with @TestParameter annotation. Thus, I can create the following snapshots for every situation I create. In this way, I have verified how my component looks in every situation, whether it is drawn properly with all its logic.
Now let’s test how our component outputs in different font sizes and different text lengths.
Again I use @TestParameter annotation to create snapshots in different font sizes, unlike other uses, I use “1.0” and “1.5” as font scale value as an array string.
@Test
fun filterSelectionView(@TestParameter(value = ["1.0", "1.5"]) fontScale: Float) {
val title = "Price range"
paparazzi.snapshot {
CompositionLocalProvider1(
LocalInspectionMode provides true,
LocalDensity provides Density(
density = LocalDensity.current.density,
fontScale = fontScale
)
) {
FilterSelectionView(
title = title,
filters = filterItems
)
}
}
}
Another benefit of snapshot testing is to see problems that may occur in different scenarios. I have a ui problem in my search field, which it also appears in this snapshot.
In this part of the article, we created and recorded our Views, this step was the record step in Snapshot testing.
2. Verify
We’ve made new improvements then we’re verifying to make sure these improvements don’t cause any undesired changes in the UI. In the verify step, the snapshots that have already been recorded are compared with the newly created snapshots, and if there is a difference, the test fails.
For example, let’s assume that the search field is not drawn somehow with the new developments in our filter component that we used above. This is something we do not want because we have lost the ability to search for filters.
./gradlew sample:verifyPaparazziDebug
When we run the command, the test will fail because there is a difference between the previously saved snapshot and the newly created snapshot.
In record and verify steps, I explained how to write our snapshot tests and compare snapshots in different scenarios. Hope this helps you with building a good, well tested components. Please leave a comment if you have any questions or feedback.