#testing #snapshots #android #compose #unidirectional
Recently I have read a great post on snapshot testing and its pitfalls by Ryan Harter ([link](https://ryanharter.com/blog/2023/10/screenshot-tests-are-manual-tests/)).
It got me thinking on how to make snapshot testing more reliable and catch regressions like Ryan describes.
Disclaimer: I firmly believe that snapshot testing alone is not enough and you do need automated tests that actually click through your app (at the very least your golden path). I cannot stress this enough.
Having said that, let's explore how your app's architecture can help make your snapshot testing more powerful and reliable.
## TLDR
Don't record snapshots of your UI in isolation. Record snapshots of your UI **before** and **after** UI interaction. Read below on how to do that.
## Enter unidirectional architecture
This post assumes you are familiar with unidirectional architectural approaches. If not, check out this post that dives more into it: [[Simple unidirectional architecture on Android]].
Now let's take Ryan's example and work from it.
Here is a state definition for that ToS screen:
```kotlin
data class ToSViewState(
val isAccepted: Boolean,
)
```
And an action to be emitted when a user accepts the terms:
```kotlin
data class AcceptToSAction(val isAccepted: Boolean)
```
And of course, we need to reduce the state, which is trivial:
```kotlin
fun reduce(state: ToSViewState, action: AcceptTosAction) =
state.copy(isAccepted = action.isAccepted)
```
Now to the testing part. I assume you use CashApp's Paparazzi but the principle will probably apply to other snapshot testing libraries as well. The trick to make this work is to combine snapshots of before and after state into one image. More details on how to set up Paparazzi this way can be found here: [[Combining several snapshots into one with Paparazzi]].
```kotlin
@Test
fun `accept tos action`() {
val initialState = TosViewState(isAccepted = false)
val nextState = reduce(initialState, AcceptToSAction(isAccepted = true))
paparazzi.snapshot {
Row {
ToSContent(
modifier = Modifier.weight(1f),
state = initialState,
)
ToSContent(
modifier = Modifier.weight(1f),
state = nextState,
)
}
}
}
```
The output of this test will be two images:
![[snapshot_delta.png]]
## How does all of this actually help?
When your designers/pms ask you to get rid of that checkbox, and you do that, and your snapshot tests fail you have a trigger point when you look at the test output. You are not just looking at the snapshots deltas. You are looking at the deltas of how state *transitions*. Seeing two images that suddenly don't make sense at all (how did that button become enabled by itself??) may nudge you to revisit the implementation and set that button to be enabled by default.