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!