Matt Brown

Thoughts on programming, media, parenting and life.

iOS UI Automation

30 September 2018

Updated: 24 October 2018

Over the past year, while working as a mobile QA engineer, I’ve learned a lot about the software development industry. Ultimately however, this role is a stepping stone in my journey to becoming an iOS developer and I’m fortunate that it has provided opportunities to directly utilize the many things I’m learning throughout that process. To that effect, this month’s dump is all about iOS UI automation.

Test Twice, Release Once

The primary goal of a QA team is to provide additional confidence in a product. Remember: it’s called quality assurance, not insertion. One of the ways this is done is early-detection of defects by automating the user interface, emulating how the end-user will interact with the system.

Unfortunately, not everyone recognizes the value of testing, while even fewer appreciate a reliable automated UI testing suite. Further still, within the camp of testing advocates you’ll find competing philosophies surrounding the best use of automation.

When manually testing a system, UI bugs can be difficult to identify for a human, especially during periods of high repetition and tedium. You find yourself succumbing to a sort of highway-hypnosis. Through the use of automation, a team can avoid this and other pitfalls of manual testing.

Difficult Lessons Learned

Before I dive into the deep end, I feel it may be necessary to outline some of the poor choices that I’ve witnessed firsthand as it pertains to iOS UI automation. My aim in sharing these issues is that the reader will be able to draw the necessary parallels to their environment and avoid similar mistakes.

1. Learn the Swift Language

This may seem obvious, but I’m not just referring to the ability to declare basic variables and functions and construct rudimentary loops. Before you invest time automating, you should understand the language and its strengths and weaknesses. Otherwise, you may spend weeks or months writing bad code that you later have to fix by doubling or tripling your effort.

There is certainly merit in the philosophy of learning through action and mistakes. In fact, author Paul Hudson’s personal motto is a favorite of mine:

Programming is an art. Don’t spend all your time sharpening your pencil when you should be drawing.

However, extending this metaphor a bit, you’re only going to get so far with a dull pencil and maybe it’s not even the right instrument for the job.

So first and foremost, make sure you understand what the language has to offer. You don’t need to be an expert, but you do need to be exposed to its capabilities. My personal recommendation for getting started with Swift is the book titled Hacking With Swift by the aforementioned author, Paul Hudson. Even if you only read the first chapter, it will serve as a robust introduction to the language.

Alternatively, if you’d prefer to hear it straight from the horse’s mouth, then you should check out the official documentation over at Swift.org. It’s a bit more sterile, but the community is extremely helpful.

2. Construct Team Standards

Once you have cultivated a solid foundation within the language, you can begin establishing the standards your code will be held to…and you’ll hold others to. The accountability offered through peer review is integral to your team’s ability to maintain this codebase months and years into the future.

An example to drive home this point is code legibility. Obviously if your team cannot understand the aim or structure of your code, how can they be expected to maintain and enhance it?

Quality is the result of a million selfless acts of care–not just of any great method that descends from the heavens.1

Assembling and adhering to team standards early on for items such as syntax and file organization, revising them as necessary, and holding each other accountable will help ensure the quality and longevity of your code.

3. Avoid Xcode Recording

Yes, Xcode includes a (seemingly) convenient way to quickly construct UI tests.
No, you should not use it.

Think of it as a siren’s call, beckoning one to their own demise as the ship of automation crashes into the rocky shoreline of confusing, single-use blocks of code, unrecognizable even to the author.

 Instead, you should familiarize yourself with breakpoints, console commands and the XCTest framework. More specifically: how to identify and reference an element within your app.


Getting Started

When creating a new Xcode project, you’re given the opportunity to include a UI testing bundle, but it’s a quick fix if you skipped that. Simply click on the project name within the Project Navigator, then tap the plus symbol at the bottom of the project and target list. Now, select the iOS UI Testing Bundle from the Test section and ensure your app is listed as both the Project and the Target to be Tested.

Note: This isn’t meant to be an exhaustive tutorial, but obviously having the necessary class and target configured within the project is an important step.

Referencing an Element

The primary construct through which you will access UI elements is the majestic accessibilityIdentifier. This property is a string declared within the development target like this:

// RootViewController.swift
view.accessibilityIdentifier = "homeView"

let primaryButton = UIButton()
primaryButton.accessibilityIdentifier = "primaryButton"

let testLabel = UILabel()
testLabel.accessibilityIdentifier = "testLabel"

Depending on the size of your app and the scope of your automation, you’ll likely end up with hundreds–maybe thousands–of these. That’s a lot of identifiers to keep track of and relying on stringly declarations across multiple targets is just asking for trouble. In fact, it seems counter-productive to force analysts to manually match strings across codebases while using a type-safe language and an IDE that supports auto-complete.

Protocols, Enums and Extensions, Oh My!

There are a handful of popular design and organizational patterns with respect to software development; however, automation frameworks tend to be ad-hoc and piecemeal. We need an efficient and extensible method for defining our identifiers. Protocols and Enums to the rescue!

// AutomationElement.swift
import Foundation

enum AutomationElement {
    enum Homepage: String {
        case primaryButton
        case testLabel
        case viewController

        var accessibilityIdentifier: String {
            switch self {
            case .viewController:
                return "homeView"
            default:
                return self.rawValue
            }
        }
    }
}

With the AutomationElement.Homepage enum, we now have a small collection corresponding to elements on the HomePageViewController and our cases match the accessibilityIdentifiers we defined earlier. Depending on how your enums are configured, you may be able to simply rely on the rawValue without even needing to explicitly define it.

At this point, we’re able to reference this central repository for identifiers across both targets, but let’s enhance it and reduce the amount of code required to write great tests. Protocols are a powerful feature within Swift, providing horizontal extensibility, predictability and increased performance. Take a look below at how you can use this tool within our example automation suite.

// Automatable.swift
import XCTest

protocol Automatable {
    var elementType: XCUIElement.ElementType { get }
    var accessibilityIdentifier: String { get }
    var elementQuery: XCUIElementQuery { get }
}

extension Automatable {
    var elementQuery: XCUIElementQuery {
        return XCUIApplication().descendants(matching: self.elementType).matching(identifier: self.accessibilityIdentifier)
    }
}

We can now conform an extension of our enum to the Automatable protocol, providing us with a predictable set of properties including the ability to reference the elementQuery directly, without needing to define it a second time.

There’s a significant caveat however: Xcode will prevent you from simultaneously importing the XCTest framework while also being part of the development target. This means that our enum’s extension must be in a different file than its initial declaration, adding a bit of complexity.

// AutomatableElement.HomePage.swift
import XCTest

extension AutomationElement.HomePage: Automatable {
    var elementType: XCUIElement.ElementType {
        switch self {
        case .primaryButton:
            return .button
        case .testLabel:
            return .staticText
        case .viewController:
            return .other
        }
    }
}

End Result

The identifiers are located within one specific file, we can reference them across targets, and access additional, intuitive properties that correspond with the UI automation process.


// RootViewController.swift
view.accessibilityIdentifier = AutomationElement.HomePage.viewController.accessibilityIdentifier

let primaryButton = UIButton()
primaryButton.accessibilityIdentifier = AutomationElement.HomePage.primaryButton.accessibilityIdentifier

let testLabel = UILabel()
testLabel.accessibilityIdentifier = AutomationElement.HomePage.testLabel.accessibilityIdentifier
// SampleProjectUITests.swift
func testExample() {
    let homePageViewController = AutomationElement.HomePage.viewController.elementQuery.element
    XCTAssertTrue(homePageViewController.waitForExistance(timeout: 10))

    [AutomationElement.HomePage.primaryButton, .testLabel].forEach { XCTAssertTrue($0.elementQuery.element.exists) }
    AutomationElement.HomePage.primaryButton.elementQuery.element.tap()
}

And there we have our first, albeit simplistic, UI test. Also, we can create additional protocols and extensions as necessary, further enriching the functionality!


Wrapping Up

Hopefully you found this helpful and inspiring, or at least informative. Feel free to download the sample project here and reach out if you have any questions. Additionally, if you enjoyed this post, you may be interested in similar posts by others such as UI Testing the Clean Way or Getting Started with Xcode UI Testing.

  1. James O. Coplien, Clean Code: A Handbook of Agile Software Craftsmanship