iOS

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

... 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.

Testing custom views

It is quite probable that the application you are testing displays custom views. And it is also very likely that several of those classes inherit from the base class UIView and not from some more specific class as e.g. UIButton. In that case – if you do not want to completely rewrite those classes and inherit from something else – you need to first make them "visible" as appropriate UI elements before you can correctly use them in UI tests. Making a custom view "visible" is largely the same process as making it accessible, meaning making it "visible" to users that use features like VoiceOver and other assistive technologies.

UI elements are accessible when they can properly describe themselves to assistive technologies. Most standard view classes from UIKit do that pretty well by default; for example, you don't have to tell a UIButton that it should "report" as a button with a certain state. When dealing with custom view classes, though – classes that most often only inherit from UIView or that use classes from UIKit in a way that was not intended – then we need to do some extra work and supply that information ourselves. To be able to do this you need to know a little bit more about the Accessibility API.

The UI Accessibility API

The UI Accessibility API mainly consists of two informal protocols, UIAccessibility and UIAccessibilityContainer, and the UIAccessibilityElement class, but we will mostly look into the UIAccessibility protocol here.

All standard UIKit controls and views implement the UIAccessibility protocol by default; this means they report their accessibility status (isAccessibilityElement) and supply additional descriptive information about themselves when asked. The attributes that describe an accessible UI element differentiate one view from another. Apple's API defines the following attributes:

  • accessibilityFrames
  • accessibilityLabels
  • accessibilityHints
  • accessibilityValues
  • accessibilityTraits

Accessibility elements provide content for accessibility frames by default because a UI element on the screen must always know about its position in a user interface. Accessibility elements also come with standard accessibility labels; accessibility hints are optional and need to be set by the user on elements that perform an action. The values set as labels and hints are used by features like VoiceOver to determine what to read to the user. They should never be used as a unique identifier in UI tests as they should be localized and should be descriptive to a user (see part 2). Setting accessibility values is optional as well, and only used when the element’s contents are changeable and cannot be described by the label.

The most important attribute for us, from a testing perspective, is the last attribute: accessibility traits. Traits describe aspects of an element’s state, usage, or behavior. Apple tells us to use the following traits to characterize elements:

  • Not Enabled
  • Selected
  • Static Text
  • Search Field
  • Keyboard Key
  • Button
  • Image
  • Summary Element
  • Link
  • Plays Sound
  • Updates Frequently
  • None

You will need to set one or several of those traits on your custom views to be able to correctly access them, either from UI test or from assistive technologies.

Setting traits on custom views

Let's assume we have a custom radio button using a basic UIView with a changing image for each state (selected / unselected). This button is not a "real" button, though; it is just a view that can look like a button depending on how it gets rendered, but it has no state.

class RadioButton: UIView {
    init() {
        [...]
    }
    func render(state: SelectionState) {
        if state.isSelected {
            image = Assets.selectedImage
        } else {
            image = Assets.unselectedImage
        }
    }
}

So even though the view might look like it has a selected and an unselected state, the accessibility API cannot tell. When trying to determine the state of our radio button in UI tests we would probably do something like this.

if radioButton.isSelected {
    radioButton.tap() // will never be called
}

This will compile, as isSelected a property that can be found on all XCUIElements. Calling isSelected on our radio button will always return false, though, even if the "button" looks selected and our code works as expected.

The reason is that UIViews aren't "visible" accessibility elements by default, as isAccessibilityElement is false by default. In that case, all traits (and other attributes like the label), which a UI element might have, will be ignored in tests as well as by assistive technologies. View classes like UIImageView, UILabel, or UIButton on the other hand have isAccessibilityElement set to true as the user of an app is expected to interact with those elements on a screen. As our radio button is subclassing from the more basic UIView we have to set that flag to true ourselves.

class RadioButton: UIView {
    init() {
        [...]
        isAccessibilityElement = true
    }
    func render(state: SelectionState) {
        if state.isSelected {
            image = Assets.selectedImage
        } else {
            image = Assets.unselectedImage
        }
    }
}

Make sure not to set this property to true on views that should not be visible to assistive technologies, though. When e.g. using a UIView as a container view that that doesn't have any functionality we would need to make sure isAccessibilityElement stays false. If we need access to this container view in UI tests we should use the accessibility identifier (see part 2), and not this property.

View classes like UIButton also come with certain pre-set accessibility traits. Those are the traits that we need to give to our custom radio button view in order to make it a "real" button. We should try to choose the best description, meaning the best combination of traits (compare list of traits on last section), for what the element does in our application. If a button is for example used to open something in Safari it would make sense to give it the trait of a Link. It is also possible to combine traits; we can for example use Button and Plays Sound together.

To be able to ask our custom radio button its state, we need to set its accessibility traits like this:

class RadioButton: UIView {
    init() {
        [...]
        isAccessibilityElement = true
    }
    func render(state: SelectionState) {
        if state.isSelected {
            image = Assets.selectedImage
            accessibilityTraits = (UIAccessibilityTraitButton | 
                                    UIAccessibilityTraitSelected)
        } else {
            image = Assets.unselectedImage
            accessibilityTraits = UIAccessibilityTraitButton
        }
    }
}

Now asking the radio button for its selected state in UI tests will work as expected.

if radioButton.isSelected {
    radioButton.tap() // will be called if selected
}

In this part of the article we covered the topic on how to use UI element attributes correctly for UI tests. As shortly mentioned, there is one very common mistake developers make in regards to the accessibilityLabel attribute of a view, namely using it to uniquely identify and access UI elements. This should not be done. We will look into this in greater detail in the next part.

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