iOS

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

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

UI testing with page objects

In part 1 and part 2 we have covered how to best describe your UI elements, how to mark them with unique identifiers and how to access them. It is now time to take a look at the wider context of UI tests: using those UI elements in automated test flows.

There are lots of ways to write UI tests and a lot of UI test I have seen in past projects were really long, hard to read (because very complex and unstructured) and quite brittle. As with any code base, abstraction can be helpful in making UI tests more manageable. At Novoda we decided to go for the Page Object design pattern to achieve this. It has traditionally been primarily applied in the context of websites (therefore the usage of page instead of screen), but has also become popular in mobile application test automation. Using pages objects works best for apps that can be easily split into separate “pages”.

So what exactly are page objects, you might ask. A page object models areas of an app's UI that its users interact with as objects within the test code. They represent a screen or a part of a screen, hide logic that should not be exposed, and expose possible actions and information the page has to offer. Exposing a stable API while encapsulating logic that manipulates the UI – and that previously live in several places in different tests – allows us to keep our tests flexible and maintainable. Applying the pattern reduces code duplication and moves logic to a single place. It establishes a clean separation between test code and page specific code. And it also makes the tests themselves cleaner and better readable because the tests themselves then only reflect the intention of the test.

“PageObjects can be thought of as facing in two directions simultaneously. Facing towards the developer of a test, they represent the services offered by a particular page. Facing away from the developer, they should be the only thing that has a deep knowledge of the structure [...] of a page (or part of a page). It's simplest to think of the methods on a Page Object as offering the "services" that a page offers rather than exposing the details and mechanics of the page.” (SeleniumHQ)

Time for an example. Imagine a simple Master-Detail app; it can be split into a main page that shows the list, and into another page that shows the detail when the user clicks on a listing.

List Detail
Screen-Shot-2018-10-30-at-13.08.07 Screen-Shot-2018-10-30-at-13.08.37

The main list page object and the detail page object then might look something like this:

class ListPageObject {
    var listTitle: String {
        let identifier = AccessibilityIdentifier.listTitle
        return XCUIApplication().staticTexts[identifier].label
    }
    var canGoToFirstListing: Bool {
        let firstCell = XCUIApplication().collectionViews.element(boundBy: 0)
        return firstCell.isHittable
    }
    func goToFirstListing() -> ListingPageObject {
        let firstCell = XCUIApplication().collectionViews.element(boundBy: 0)
        firstCell.tap()
    }
}
 
class DetailPageObject {
    var detailTitle: String {
        let identifier = AccessibilityIdentifier.detailTitle
        return XCUIApplication().staticTexts[identifier].label
    }
    func goBackToList() {
        let navigationBar = XCUIApplication().navigationBars.firstMatch
        let backButton = navigationBar.buttons["Back"]
        backButton.tap()
    }
}

Page objects may not always cover the whole visible screen, thought. A good example would be a tab bar in a tabbed application.

class TabControllerPageObject {
    func goToFirstTab() -> SomePageObject {
        let tabBarButtons = XCUIApplication().tabBars.buttons
        tabBarButtons.element(boundBy: 0).tap()
    }
    func goToSecondTab() -> SomeOtherPageObject {
        let tabBarButtons = XCUIApplication().tabBars.buttons
        tabBarButtons.element(boundBy: 1).tap()
    }
}

Screen-Shot-2018-10-30-at-13.05.43

In the current state of our page objects, a possible test could look like this:

func testThatFirstListingIsClickable() {
    TabControllerPageObject().goToSecondTab()
    let list = ListPageObject()
    expect(list.canGoToFirstListing) == true
}

In the test we are clicking on the right of two tabs, there we expect a list view controller with at least one listing and we check that we can click on the first listing. This test is already quite, compared to what it could have look without using page objects:

func testThatFirstListingIsClickable() {
    let tabBarButtons = XCUIApplication().tabBars.buttons
    tabBarButtons.element(boundBy: 1).tap()
    let firstCell = XCUIApplication().collectionViews.element(boundBy: 0)
    expect(firstCell.isHittable) == true
}

But we can do even better. Instead of assuming that when clicking on the second tab we get shown the list view controller (which is implicit knowledge), we can introduce flows that combine different page objects into one coherent structure. How can we do that? By letting the page objects return new objects after a transition occurred.

class TabControllerPageObject {
    [...]
    func goToSecondTab() -> ListPageObject {
        let tabBarButtons = XCUIApplication().tabBars.buttons
        tabBarButtons.element(boundBy: 1).tap()
        return ListPageObject()
    }
}

class ListPageObject {
    [...]
    func goToFirstListing() -> ListingPageObject {
        let firstCell = XCUIApplication().collectionViews.element(boundBy: 0)
        firstCell.tap()
        return ListingPageObject()
    }
}

class DetailPageObject {
    [...]
    func goBackToList() {
        let navigationBar = XCUIApplication().navigationBars.firstMatch
        let backButton = navigationBar.buttons["Back"]
        backButton.tap()
    }
}

Our test example would then look like this:

func testThatFirstListingIsClickable() {
    let list = TabControllerPageObject().goToSecondTab()
    expect(list.canGoToFirstListing) == true
}

Our test case now reads so well that even non-engineers can figure out what is going on here.

Well, this is it. I hope you enjoyed reading this little series and that I could help clearing some obstacles out of the way. If you would like to learn more about testing and mobile applications have a look at our other post.

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