iOS

Tricks & treats to make UI testing less terrifying (part 2)

... on a quest to get to the bottom of and promote mobile application security issues.

This series will cover a couple of obstacles or complications you might run into when writing UI tests for iOS and will give advice on how to write cleaner tests. In the first part we will look into possible issues with testing custom views; the second part will explain how to correctly access views in tests; the last part will finally show how you can use page objects to structure your tests in a sustainable way.

Accessibility labels vs. identifiers

In this second part of the article we want to look into how to correctly access view elements in UI tests. As shortly mentioned in part 1, there is one very common mistake developers make here: using the accessibilityLabel attribute of a view to uniquely identify UI elements. This should not be done. Let’s see why.

Accessibility labels as “identifiers”

Accessibility labels are used by features like VoiceOver to determine what is read to the user. They should be localized, so users that rely on assistive technologies can use the app in different languages, too. This requirement makes them very poor identifiers – however, as UI tests are often only run in one language, this shortcoming does not always manifest itself. But even then, using accessibility labels as identifiers in tests makes your live harder than necessary! Texts in apps can change quite a bit from release to release. Every time a string that is also used as an identifier changes, your tests break. What can we do?

Unique identifiers

For UI testing purposes Apple provides us with a so-called accessibilityIdentifier. It is meant to "be used to uniquely identify a UI element in the scripts we write using the UI Automation interfaces" (Apple's documentation). Accessibility identifiers aren't accessed by e.g. VoiceOver. They are part of the UIAccessibilityIdentification protocol, which consists of (a) method(s) that associate a unique identifier with elements in a user interface. The only strict requirement for adhering to this protocol is defining the accessibilityIdentifier property.

Setting accessibility identifiers is very easy. In our view's initializer we simply do:

someView.accessibilityIdentifier = "some_string"

In UI tests, we can then find our view like this

let someView = XCUIApplication().otherElements["some_string"]

or maybe like this

let someView = XCUIApplication().buttons["some_string"]

... depending on what type of UI element someView is.

To avoid typos and confusion we suggest using static strings as identifiers.

public struct AccessibilityIdentifier {
    static let someString = "some_string"
}

Just have a struct somewhere with all identifiers

let someView = XCUIApplication().otherElements[AccessibilityIdentifier.someString]

... which will then be accessible from inside all UI tests.

Accessibility identifiers for system views

Problems start when trying to set identifiers on views we only have limited control over, meaning view classes that the system (at least partly) manages for us, like bar button items. Let's have a look at some system UI elements.

System UI elements in tab bars, navigation bars, and search bars

All three bar classes – tab bars, navigation bars, and search bars – are UI elements provided by Apple. They are accessible by default and conform to the UIAccessibilityIdentification protocol so we can set an identifier like this:

tabViewController.tabBar.accessibilityIdentifier =
    AccessibilityIdentifier.mainTabBar
navigationController.navigationBar.accessibilityIdentifier =
    AccessibilityIdentifier.featureNavigationBar
searchController.searchBar.accessibilityIdentifier =
    AccessibilityIdentifier.listSearchBar

Navigation bars differ slightly from the other two classes insofar as they manifest special behavior if no custom accessibility identifier is set. If no custom identifier is set, the default identifier of the navigation bar automatically becomes the same as the title of the bar. (Keep in mind that in this special case the identifier changes if the title is changed.)

Tab bars and navigation bars are treated as special UI elements in tests. So in addition to the above mentioned approach they are also accessible by group:

let app = XCUIApplication()
let mainTabBar = app.tabBars[AccessibilityIdentifier.mainTabBar]
let mainNavigationBar =
    app.navigationBars[AccessibilityIdentifier.featureNavigationBar]

For some reason we do not get searchBars as a group; interestingly Apple provides us with searchFields, though.

The fun starts when trying to access system UI elements in those bars. Let's first look into navigation bar titles.

Navigation bar title views come with the bar itself and do not need to be managed by us, except for being given a value. Unfortunately, we can neither set a custom accessibility identifier on them, nor do they come with a default one. Simply setting an accessibility identifier will compile (because title views are just UIViews when it comes down to it)

navigationItem.titleView?.accessibilityIdentifier =
    AccessibilityIdentifier.featureNavigationBarTitle

... it will however have no effect. The problem with that behavior is that we cannot easily assert on the title of a view controller in tests. As navigation bars cannot have more than one title, that problem can be worked around by setting an identifier on the bar and then asking it for its title.

let identifier = AccessibilityIdentifier.featureNavigationBar
let navigationBar = XCUIApplication().navigationBars[identifier]
navigationBar.title

The buttons that come with the bar are more complicated, though, as there can be several and different types.

Bar button items and tab bar buttons

Bar button items can mostly be found on navigation bars, but also for example on search bars. They inherit from UIBarItem; both classes theoretically conform to UIAccessibilityIdentification. Like with the title views, it is possible to set an accessibility identifier on such an item

navigationItem.backBarButtonItem?.accessibilityIdentifier =
    AccessibilityIdentifier.featureNavigationBarBackButton
navigationItem.leftBarButtonItem?.accessibilityIdentifier =
    AccessibilityIdentifier.featureNavigationBarBackButton

... but again with no effect. To access the bar buttons we have two (or maybe three) possibilities. We can either use the button label, e.g. Cancel, to find it.

let navigationBarButtons = XCUIApplication().navigationBars.buttons
let cancelButton = navigationBarButtons["Cancel"]

However, as mentioned above, this solution is not very stable as the title label can change, e.g. when we switch the app's language. If we need a solution that works for several localizations we can use XCUIElementQuery.element(boundBy: 0) instead.

Both approaches also work for search bar buttons (which technically are UIBarButtonItems, too) and for tab bar buttons (which also inherit from UIBarItem).

let tabBarButtons = XCUIApplication().tabBars.buttons
let homeButton = tabBarButtons["Home"]
// or
let homeButton = tabBarButtons.element(boundBy: 0)

A third solution would be creating and setting customs items as bar buttons items, as this gives us more control over those items.

let backButton = UIBarButtonItem(
                    title: String.Localized.back,
                    style: .plain,
                    target: self,
                    action: #selector(goBack)
backButton.accessibilityIdentifier =
    AccessibilityIdentifier.featureNavigationBarBackButton`
navigationItem.leftBarButtonItem = backButton

The third approach is far more work, though as we do not need to create custom bar buttons most of the time.

In this part of the article we covered the topic on how to mark UI elements with unique identifiers and how to access them. In the next part of this series we will take a look at the wider context of UI tests, namely how to best use UI elements in automated test flows.

Read the next part of this series.

Enjoyed this article? There's more...

We send out a small, valuable newsletter with the best stories, app design & development resources every month.

No spam, no giving your data away, unsubscribe anytime.

About Novoda

We plan, design, and develop the world’s most desirable software products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact