Extracting Lazily Loaded Elements for UI Testing in a SwiftUI iOS App

Build robust SwiftUI apps

Andronick Martusheff
Better Programming

--

Lazy loading is a powerful optimization technique used by online apps where data is loaded on-demand, and not monolithically. Online apps are constantly sending and receiving data, and any means of cutting down the amount of unnecessarily transacted data is beneficial to both the end-user and the app itself. The app’s backend servers receive less strain, and an end-user isn’t hung up on waiting for data to load. So what’s the problem?

Quick note: if you’d like to skip ahead right to the function & its implementation, skip ahead to “Creating out Custom Swipe Function”

Challenges in Finding Lazily Loaded Elements

When running automated UI tests for iOS, it’s extremely important to note that we are essentially bound to what’s provided by the app’s accessibility hierarchy. An accessibility hierarchy, in terms of iOS and XCUITest, is a textual representation of the app's user interface. It’s what we’re testing against when using assertion functions to check for the existence of elements in our automated UI tests.

It is effectively our source of truth, and an element set up in-app code can be included or excluded from the hierarchy by having its status as an accessibility object changed. The issue resulting from lazily loaded views and stacks is that an expected element may or may not be loaded. If it has yet to be loaded, it may have the potential to exist, but as far as your UI test is concerned, it does not. We need a way to find what we need.

Example Accessibility Hierarchy: LazyVStack vs. VStack

Shown above is the accessibility hierarchy for the application we will be testing. These hierarchies represent what is known of the stack containing the green cells. When capturing a snapshot of this hierarchy, for both the lazily and normally loaded VStacks, we’re in the exact same application state; except, one is providing much more information.

Our example here in grey stretches down and lists all 99 elements the VStack contains, while the top in black lists just what’s loaded in the LazyVStack. For testing purposes, checking for the existence of cell ’n’ in the regular VStack will be very easy, everything is loaded! However, it’s highly unlikely you’ll come across such a test-friendly situation in the real world.

Objectives

  • How do we find an unloaded cell?: We will find it by swiping in the direction of the expected element. Doing so updates the hierarchy.
  • How do we find it safely?: We create a custom, configurable, swiping method that searches for our target element until it is found.

The reason we want to create a custom swipe function as opposed to using the existing swipe{Direction}() function is that using the default forces us to hard code a set number of swipes before reaching an expected value. Instead, we want something that can simulate the behavior of a user and find the desired element.

The source code for this application is available to be cloned here.

Getting Familiar With the App

This app (GIF’d below) has 4 main sections, all relevant and exemplary of the issue we discovered earlier.

  1. Simple Text Header “My Custom Scroll View”
  2. LazyVStack (Green)
  3. LazyHStack (Red)
  4. Auto-Scroll Buttons, in order; beginning of green stack, end of the green stack, beginning of red stack, end of the red stack.

This application gives us a playground to test our swipe function in all 4 directions.

Setting up our Testing Environment

Below is the starter code that can also be found on the main branch of the repository linked above. We’re setting up a very simple page object model design pattern here before proceeding with the tests.

If you would like to hop directly into the final code, checkout the “completed” branch from the linked repo.

Here we are working with 3 main classes:

  1. LazyLoadUITests: Our UI test class where our test functions live and are executed.
  2. BasePage: A base page with reference to our application.
  3. LazyLoadPage: An extension of BasePage where everything related to the LazyLoad screen will live.

This is a very quick example of POM (Page Object Model). A high-level description of POM goes as follows: keep everything related to element declaration & property retrieval in the page classes (BasePage, LazyLoadPage) leaving the UI test class to handle assertions, with the help of the page classes.

Creating our Custom Swipe Function

  1. element: the first parameter being passed in is the target element, or in other words, the element we are trying to find. If found, the function will return and cease to swipe up.
  2. maxAttempts: the maximum number of times we’d like to swipe on the view. This is configurable and prevents a situation where a test is forever stuck looking for something that does not exist.
  3. velocity: the number of pixels we will be swiping through per second.

Our custom swipe function is going to piggyback off of the existing swipe{Direction}() function provided by XCTest. We are going to repeat this action until one of 2 things happen; the target element is found, or we’ve reached our maximum number of attempts. The element we are swiping on is self because we are extending off of an XCUIElement.

What this means is that the element we are going to call this helper function on is the view/element that contains the target view. Also, notice that once found the function doesn’t return anything… this is intentional. Much like the original swipe{Direction}()function, it works independently. An example of a test full flow, with the included use of swipeDownTo(...) , can be seen below.

And here’s the function in full action!

  • Start by seeing if #1 exists.
  • Use our created function to swipe to #67 until found, then check for it.
  • Tap on the down button to take us to the bottom of the stack.
  • Check if #99 exists.
  • Use a similar function to swipe down until #17 is found, then check for it.
  • Tap on the up button to take us back to the top of the stack.
  • Check if #1 exists.

Feel free to optimize, and make it exactly what you need it to be! I’ve found this function to be extremely helpful and prevent the need for constant updates to flaky tests that we're failing as a result of views growing in size. As the cells within these views grew in number/size, the default swipeUp() was no longer getting the simulator to where it needed to be to complete testing. This function offers some flexibility.

Challenge

You may notice I did not complete swipe left/right portion of this in the article. I’m leaving this as a challenge, though if you want the solution, it will be available on the completed branch.

Repo Link:

Hints/Tips:

  • Put a breakpoint on a line within the test function and enter po print(XCUIApplication().debugDescription) to print the app’s accessibility hierarchy, and get access to the element’s identifiers.
  • Update your bundle identifier & team under “Signing and Capabilities” to your info should you experience any build errors upon cloning the repo.

--

--

Let’s learn together! iOS Engineering & Automation @ SBUX. B.S. Software Engineering @ ASU ‘23