UI Testing iOS application with EarlGrey


May 12, 2020

#swift #testing

As a mobile developer, you spend most of your time on either creating a new feature or changing one that already exists. Eventually when these changes are introduced, there comes the time to verify that the application still works as expected. This can be done manually, but on the long run manual approach becomes time-consuming and error-prone. A better option is to create user interface (UI) tests so that user actions are performed in an automated way.

In this article we’re going to take a look at the UI testing on iOS and learn how to write clean and concise UI tests using EarlGrey framework.

What about Apple’s official UI Testing Framework?

Probably every iOS developer wrote UI tests for iOS application with XCUI. It seems to be a nice out-of-the-box option, but there are certain issues you’ll notice as you go. Here are just some of them:

  • can be used for black-box testing only.
  • provides limited options to interact with the application under test and define the application state (you can use launch parameters though).
  • uses time-based and condition-based clauses to wait for asynchronous actions. This leads to cumbersome testing code and flaky UI tests.
  • doesn’t offer a reliable way to check the element’s visibility.
  • is quite slow, because tests are performed by the test runner application that communicates with the main application via IPC. Frequently it takes quite some time just to launch the application, reach the desired screen and make a simple user interaction such as scroll.

Some of the issues mentioned above could be solved by writing extra code and using certain tools. I’d say that XCUI is a good option to start with UI testing on iOS, but we can do better. Let’s dive into details!

UI testing with EarlGrey

EarlGrey is a white-box functional UI testing framework for iOS developed by Google. Unlike XCUI, EarlGrey uses Unit Testing Target and provides powerful built-in synchronizations of UI, network requests, etc. that helps you to write tests that should be easy to read and maintain (no waiting clauses).

It should be mentioned that this article covers EarlGrey v1.0, but most of it should be applicable for v2.0 as well. You can find lots of similarities between EarlGrey and Espresso testing framework, which is widely used on Android.

EarlGrey framework is based on three main components:

  • Matchers - locates a UI element on the screen;
  • Actions - executes actions on the elements;
  • Assertions - validates a view state.

Taking this into account, we can construct a basic EarlGrey test as following:

EarlGrey
    .selectElement(with: <GREYMatcher>) ➊
    .perform(<GREYAction>) ➋
    .assert(<GREYAssertion>) ➌

➊ Selects an element to interact with.
➋ Performs an action on it.
➌ Makes an assertion to verify state and behavior.

Accessibility Identifiers

UI testing on iOS is built on top of accessibility. Similarly to XCUI, EarlGrey uses accessibility identifiers to find a UI element on the screen. We just have to define ones as static constants, assign them to the dedicated UI components and call grey_accessibilityID() to select a UI element from our test.

Stubbing Network Calls

It is important to note that you might face different issues while running UI tests against real webserver: unstable network connection, backend changes, slow UI tests, etc. A better approach would be to use fake network data in our tests(stubbed from file). There are different ways of doing it:

  • Creating and injecting a Network Service mock;
  • Subclassing NSURLProtocol abstract class, that is responsible for the loading of URL data;
  • Running a web server like GCDWebServer or swifter on the localhost.

For a sake of simplicity we’ll create the Network Service mock and use one instead of a real implementation.

Using Fake AppDelegate

When you run UI tests, the application is launched and AppDelegate with the rest of UI component are instantiated. The business logic performed on startup might slow down your tests and result in unexpected behaviour. For this reason, we should use a fake AppDelegate instance while running UI test. Here is the way to create and inject one in main.swift:

import UIKit

private let fakeAppDelegateClass: AnyClass? = NSClassFromString("TMDBTests.FakeAppDelegate")
private let appDelegateClass: AnyClass = fakeAppDelegateClass ?? AppDelegate.self

_ = UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))

Writing a test using EarlGrey

Let’s write UI tests for the TMDB application, that I’ve described in the previous article.

First we’ll configure EarlGrey from the test’s setUp function:

    override func setUp() {
        GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeyAnalyticsEnabled) ➊
        GREYConfiguration.sharedInstance().setValue(5.0, forConfigKey: kGREYConfigKeyInteractionTimeoutDuration) ➋
        GREYTestHelper.enableFastAnimation() ➌
    }

➊ Disable Google analytics tracking.
➋ Use 5s timeout for any interaction.
➌ Increase the speed of your tests by not having to wait on slow animations.

Next, let’s create a functional UI test for the Movies Search screen:

func test_startMoviesSearch_whenTypeSearchText() {
    // GIVEN
    let movies = Movies.loadFromFile("Movies.json")
    networkService.responses["/3/search/movie"] = movies
    open(viewController: factory.moviesSearchController(navigator: moviesSearchNavigator))

    // WHEN
    EarlGrey
        .selectElement(with: grey_accessibilityID(AccessibilityIdentifiers.MoviesSearch.searchTextFieldId))
        .perform(grey_typeText("joker"))

    // THEN
    EarlGrey.selectElement(with: grey_accessibilityID(AccessibilityIdentifiers.MoviesSearch.tableViewId))
        .assert(createTableViewRowsAssert(rowsCount: movies.items.count, inSection: 0))
}

This test makes sure that movies search is triggered and results are shown when the user types in some text in the search bar. I’ve used the Given-When-Then structuring approach to make the test more readable. You can find more in-depth explanation of the approach in this article by Martin Fowler.

The test is quite self-explanatory, you should understand most of it even without previous experience with EarlGrey. As you might notice, we didn’t use waiting clauses like waitForExpectationsWithTimeout:handler: in this test, because EarlGrey performs all required synchronizations under the hood.

EarlGrey is designed with extensibility in mind, giving space for customization. In the test above we’ve used a custom assertion to check number of rows in a tableView. The assertion is created using GREYAssertionBlock that accepts the assertion logic in a closure:

    private func createTableViewRowsAssert(rowsCount: Int, inSection section: Int) -> GREYAssertion {
        return GREYAssertionBlock(name: "TableViewRowsAssert") { (element, error) -> Bool in
            guard let tableView = element as? UITableView, tableView.numberOfSections > section else {
                return false
            }
            return rowsCount == tableView.numberOfRows(inSection: section)
        }
    }

Utilizing the Page Object Model Pattern

A common challenge when writing tests is to make them more readable and maintainable. This can be achieved by using the Page Object Model pattern, that was introduced by Martin Fowler. It is a fantastic way to have the separation of concerns: what we want to test and how want to test it. Eventually you can access UI elements in a readable and easy way, avoid code duplication and speed up development of test cases.

Let’s define a Page class, that will serve as a base class for other page objects. The class has a function on that creates a Page instance by type T. Function verify is used to check that a page is visible and should be overridden in a subclass:

class Page {

    static func on<T: Page>(_ type: T.Type) -> T {
        let pageObject = T()
        pageObject.verify()
        return pageObject
    }

    func verify() {
        fatalError("Override \(#function) function in a subclass!")
    }
}

Next we’ll create a MoviesSearchPage class that provides actions and assertion for this page:

class MoviesSearchPage: Page {

    override func verify() {
        EarlGrey.selectElement(with: grey_accessibilityID(AccessibilityIdentifiers.MoviesSearch.rootViewId)).assert(grey_notNil())
    }
}

// MARK: Actions
extension MoviesSearchPage {

    @discardableResult
    func search(_ query: String) {
        EarlGrey
            .selectElement(with: grey_accessibilityID(AccessibilityIdentifiers.MoviesSearch.searchTextFieldId))
            .perform(grey_typeText(query))
        return self
    }
}

// MARK: Assertions
extension MoviesSearchPage {

    @discardableResult
    func assertMoviesCount(_ rowsCount: Int) -> Self {
        EarlGrey.selectElement(with: grey_accessibilityID(AccessibilityIdentifiers.MoviesSearch.tableViewId))
            .assert(createTableViewRowsAssert(rowsCount: rowsCount, inSection: 0))
        return self
    }
}

With the above-mentioned code in place we can now simplify our test:

func test_startMoviesSearch_whenTypeSearchText() {
    // GIVEN
    let movies = Movies.loadFromFile("Movies.json")
    networkService.responses["/3/search/movie"] = movies
    open(viewController: factory.moviesSearchController(navigator: moviesSearchNavigator))

    // WHEN
    Page.on(MoviesSearchPage.self).search("joker")

    // THEN
    Page.on(MoviesSearchPage.self).assertMoviesCount(movies.items.count)
}

Same approach could be used to test the initial state of the screen:

func test_intialState() {
    // GIVEN /WHEN
    open(viewController: factory.moviesSearchController(navigator: moviesSearchNavigator))

    // THEN
    Page.on(MoviesSearchPage.self)
        .assertScreenTitle("Movies")
        .assertContentIsHidden()
        .on(AlertPage.self)
        .assertTitle("Search for a movie...")
}

Just like that, we’ve written functional UI tests for our application, that should be easy to read and maintain.

Conclusion

Automated testing should be an integral part of the development lifecycle. Getting a habit of creating UI tests is crucial when it comes to fast verification that the UI of your app is functioning correctly. This approach allows you to quickly and reliably check that application meets its functional requirements and achieves high standards of quality.

You can find the project’s source code on Github. Feel free to play around and reach me out on Twitter if you have any questions, suggestions or feedback.

Thanks for reading!