Select Page

As an iOS developer, it’s crucial to translate the design provided by UX/UI designers into a functional mobile app. A single screen also can adopt various UI configurations, such as hiding views or adjusting layouts based on different available data.

But how do you ensure the screen displays correctly with complex UI requirements? An efficient method to guarantee the correctness of your app’s UI is through the implementation of snapshot tests.

Snapshot tests capture an image of your app’s UI, which is then compared to a previously validated snapshot. Moreover, there are additional benefits to incorporating snapshot tests into your development workflow:

  1. Faster UI Development Iteration:
    • Unlike running the simulator, snapshot tests provide a quicker iteration cycle, saving valuable development time.
  2. Ensuring Correctness Across Different UI Configurations:
    • Snapshot tests allow you to validate the correctness of various UI configurations simultaneously.
  3. Effortless Mocking of Different Data:
    • Easily simulate different presentation data using individual test.
  4. Preventing Undesired Changes to UI:
    • By comparing UI changes between the current and previous test runs, snapshot tests act as a safeguard against unintentional changes.

Case Example

Consider a user profile screen within your app. A user may have the following properties and some properties are optional:

  1. Full name
  2. Image (Optional)
  3. Multiple Phone numbers (Optional)
  4. Address (Optional)
  5. Email

This is the design provided by UI designer:

The design appears simple at first glance. However, depending on the available properties, various UI configurations may exist which means there are different ways in which the UI could be displayed to the user.

For instance, the UI will vary based on the following conditions:

  1. View with optional properties should be hidden when the property is not available.
  2. Max number of lines for name is 2
  3. Max number of lines for address is 4
  4. A user can have multiple phone numbers

Snapshot tests can be used to track these different UI configurations.

Now, let’s look into a Swift code example:

UI Code

The UserViewModel is a class that holds presentation data for the UI, while UserDetailVC is a class responsible for displaying the UI based on the UserViewModel. The code is provided below; for simplicity, we have omitted the implementation details of the UI.

public struct UserViewModel{
    let fullname: String
    let email: String
    let image: UIImage?
    let phoneNumbers: [String]?
    let address: String?
}

public class UserDetailVC: UIViewController{
    public let viewModel: UserViewModel
    
    public init(viewModel: UserViewModel) {
        viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    //Other unrelated variables are omitted
    //View are displayed based on the viewModel data
}

Snapshot Test Code

To prepare for snapshot tests, we utilize the Swift Snapshot Testing library. Here is how the test file looks with a single test to assess the UI with all user properties available. Additional information about the code is provided in comments within the code.

import XCTest
import SnapshotTesting
import UserDetailExample

final class UserDetailSnapshotTests: XCTestCase {

    func test_user_withAllProperties(){
        //We call a helper method to create an image with simply red color here. A simple image ensures that our tests running faster.
        let image = UIImage.make(withColor: .red)
    
        //Create view model to be displayed
        let user = UserViewModel(fullname: "Any name",
                                 email: "any@email.com",
                                 image: image,
                                 phoneNumbers: ["any phone"],
                                 address: "any address")
        
        //Make system under test (SUT) in this case, the SUT is UserDetailVC
        let sut = makeSUT(userViewModel: user)
        
        //This is a function by Swift Snapshot Testing library. You can specify which device dimension to run to, and record or compare it with previously saved screen.
        assertSnapshot(matching: sut, as: .image(on: .iPhone12), record: true)
    }
    
    // MARK: - Helpers
    
    private func makeSUT(userViewModel: UserViewModel) -> UserDetailVC {
        let sut = UserDetailVC(viewModel: userViewModel)
        return sut
    }
}

When running the snapshot test for the first time, we need to set record = true to capture the screen UI and save it to disk. We will encounter a test error as shown in the image below, although this is the expected behaviour.

When clicked at the recorded url destination, we can then open the image to see the recorded snapshot.

When we run the test again with record = false, we will get a successful test, indicating that the current UI we are testing is the same as the previously recorded snapshot. If you want to record a new snapshot, you can set record = true again.

Testing Different UI Configurations

Now that you understand how snapshot tests work, we can add more tests for different UI configurations. These are the display behaviours that we want to test:

  • The view with optional properties should be hidden when the property is not available.
  • The maximum number of lines for the name is 2.
  • The maximum number of lines for the address is 4.
  • Use the plural title “Phones” when we have 2 or more phone numbers.
  • A user can have multiple phone numbers.

We can combine multiple behaviours into a single test, which can save disk space and test running time. Finally, we have four tests, as shown in the code below:

import XCTest
import SnapshotTesting
import UserDetailExample

final class UserDetailSnapshotTests: XCTestCase {

    func test_user_withAllProperties(){
        //We call a helper method to create an image with simply red color here. A simple image ensures that our tests running faster.
        let image = UIImage.make(withColor: .red)
    
        //Create view model to be displayed
        let user = UserViewModel(fullname: "Any name",
                                 email: "any@email.com",
                                 image: image,
                                 phoneNumbers: ["any phone"],
                                 address: "any address")
        
        //Make system under test (SUT) in this case, the SUT is UserDetailVC
        let sut = makeSUT(userViewModel: user)
        
        //This is a function by Swift Snapshot Testing library. You can specify which device dimension to run to, and record or compare it with previously saved snapshot.
        assertSnapshot(matching: sut, as: .image(on: .iPhone12), record: true)
    }
    
    func test_user_withImage_noOtherOptionalProperties(){
        let image = UIImage.make(withColor: .red)
        let user = UserViewModel(fullname: "Any name",
                                 email: "any@email.com",
                                 image: image,
                                 phoneNumbers: nil,
                                 address: nil)
        
        let sut = makeSUT(userViewModel: user)
        assertSnapshot(matching: sut, as: .image(on: .iPhone12), record: true)
    }
    
    func test_user_withImage_longNameLongAddress_onePhoneNumber(){
        let image = UIImage.make(withColor: .red)
        let user = UserViewModel(fullname: "This is a long text to test user with a very long name to ensure it is displayed correctly.",
                                 email: "any@email.com",
                                 image: image,
                                 phoneNumbers: ["0166669999"],
                                 address: "First line address,\nSecond line,\nThird line,\nFourth line,\nFifth line")
        
        let sut = makeSUT(userViewModel: user)
        assertSnapshot(matching: sut, as: .image(on: .iPhone12), record: true)
    }
    
    func test_user_noImage_longName_twoPhoneNumbers(){
        let user = UserViewModel(fullname: "This is a long text to test user with a very long name to ensure it is displayed correctly.",
                                 email: "any@email.com",
                                 image: nil,
                                 phoneNumbers: ["0166669999", "0135556666"],
                                 address: nil)
        
        let sut = makeSUT(userViewModel: user)
        assertSnapshot(matching: sut, as: .image(on: .iPhone12), record: true)
    }
    
    // MARK: - Helpers
    
    private func makeSUT(userViewModel: UserViewModel) -> UserDetailVC {
        let sut = UserDetailVC()
        sut.viewModel = userViewModel
        return sut
    }
}

Here are the recorded snapshots with its corresponding test name for final and completed UI:

Recorded snapshots for user detail screen with different UI configurations

Note that we are only showing the final result here. The process of testing snapshot is usually done iteratively

  1. Write or improve UI code.
  2. Run snapshot tests.
  3. Open and review recorded snapshots to verify UI correctness.
  4. If the UI appears visually incorrect, repeat the process until it displays as expected

Tips: Since the saved snapshots are standard images, you can open all of them at once in the Preview app. This way, you can review any changes when you run subsequent tests without having to reopen them individually. This allows you to quickly observe any alterations in different UI configurations.

Conclusion

Snapshot tests are a valuable technique to efficiently ensure that your app’s UI is displayed as expected. By eliminating the need to run the simulator for every UI check, especially for screens deeply embedded in the app, they save valuable time. Additionally, they act as a safeguard against unintended changes and the ability to effortlessly test the UI with various configurations further boosts their effectiveness.