mastering iOS UI testing visual

As software developers, we know how important it is to write automated tests to make stable and performant apps for our partners. Particularly important is to test areas of our app that are responsible for core logic and decision making. Unit tests like these (so called because they test a single isolated function or ‘unit’ of code) are a crucial part of software development and personally help me sleep at night. They are typically not difficult to write and are robust; if they unexpectedly fail in the future, it is likely because something has changed or gone wrong in the codebase they are testing rather than an issue with the test itself.

However, one area of testing that is not as straightforward is UI testing.

What is UI Testing and the main challenges

Instead of testing your business logic UI tests test the code surrounding your views which helps to ensure the quality and correctness of your user interfaces.

Writing and maintaining UI tests can be complex and the resultant tests can be fragile. iOS app interfaces often involve intricate hierarchies of views, and small changes in the UI structure or layout can break existing tests. As a result, these tests often need frequent updates, and as such can be time-consuming to maintain.

Another disadvantage is that UI tests tend to be slower to run than unit tests, as they require interacting with the app’s user interface, often involving real-time animations and network requests that either have to be emulated or run on a physical device.

Exploring snapshot testing in a hack day

As part of the recent Rant hack day exploring mobile testing I decided to investigate snapshot testing with iOS, a technique that promises to help mitigate issues with speed and brittleness.

What is snapshot testing

Snapshot testing is a way to automatically capture and compare the visual output of your application’s  user interface components, such as views, with screens with previously approved “snapshots” of the same components. Its primary purpose is to detect any unintended visual changes, or “regressions,” that might occur when you make changes to your app’s code or user interface.

These are the key elements of snapshot testing:

  • Capture and Compare: Snapshot testing involves capturing screenshots or images of specific views or UI components within your app under controlled conditions. These screenshots are referred to as “snapshots.”
  • Comparison to Baseline: Once snapshots are captured, they are compared to a baseline set of snapshots that represent the expected or “correct” appearance of the UI. The baseline snapshots serve as a reference point.
  • Detection of Visual Regressions: During testing, if there are differences between the captured snapshots and the baseline snapshots, it indicates a visual regression. Visual regressions are unintended UI changes that might have been introduced due to code modifications.

Testing out snapshot testing

To test out snapshotting I used a library developed by PointFree called SnapshotTesting that would take care of capturing the snapshots and then compare them with the stored baseline version. There are a few different snapshot testing libraries out there, one of the reasons I chose SnapshotTesting is that it is integrated very well with The Composable Architecture (TCA), a framework also made by PointFree which we use. But you can also use SnapshotTesting without TCA.

The first step was to import the SnapshotTesting dependency using Swift Package Manager.

Then it was a matter of:

  1. Creating a new XCTestCase
  2. Assigning the view you want to test as a constant, making sure the view has been set to the correct state. In this example I’m assigning a newly initialised viewModel with its default state.
    let view = MainView(viewstore: ViewModel())
  3. Then put this view into a navigationView property
    let navView = NavigationView { view }
  4. Then use the assertSnapshot func to create / test a snapshot
    assertSnapshot(
        of: navView,
        as: .image(layout: .device(config: .iPhone13Pro))
    )

    The above code example is using SwiftUI

Now when you run the test the first time the first baseline snapshot will be taken and subsequent tests will compare that new snapshot to the baseline. If the snapshots differ then the test fails, otherwise they will pass.

This was a very simple example but I also explored adding additional snapshot steps to the same test by changing the state in the viewModel (after assertSnapshot  ) and then took another screenshot of the updated view. This was very helpful to test how the view would update when changes to the state were made.

In conclusion

I found creating the snapshot tests super easy. They ran incredibly quickly, especially compared to traditional UI testing that sometimes has to run in real time. And most importantly I found the tests to be reliable. In UITests, I have found that tests are not always consistent, for example, they sometimes miss pressing an action button. But every time I ran the same snapshot test it consistently took a snapshot of the correct state.

These results have given me confidence that we could easily integrate this type of UI testing into our production apps and at the same time have the confidence that they are going to be reliable.

The only caveat is that if you update your UI in any way that will cause the next snapshot test to differ from your initial baseline snapshot, you will need to reset your baseline snapshot; otherwise, the tests will fail. But in my mind this is a good failsafe to help make sure that changes to UI were intended.

 

Click to learn more of our iOS app developments