Simplifying iOS UI Testing with Page Object Model
Why bother to write tests?
Let’s start with the basics, why do we even need to write tests?
Even though I’m a big proponent of writing tests, I do understand it’s sometimes the case that not enough time or resources can be dedicated to it, due to various reasons, most common being a lack of understanding from the powers that matter, or in other words a lacking company culture.
It’s usually us, developers, who have to convince our managers that we need to write tests.
On the other hand, there’s a substantial number of developers who simply don’t believe in writing tests, or they just don’t like writing them, for one reason or another.
If you are one of those developers or managers, I would ask you to carefully consider the following facts.
Hand washing in hospitals is a fairly recent practice.
It was only in 1847 that Hungarian doctor Ignaz Semmelweis proposed the idea of medical staff washing their hands in order to reduce mortality rates in hospitals.
His ideas conflicted with the established scientific and medical opinions of the time and his ideas and were outright rejected by the medical community of the time.
Because of rejection and ridicule he was exposed to, he suffered a nervous breakdown, and died in an asylum.
It took a long time for the medical community to start using hand washing as a mandatory practice.
Needless to say, since then, hand washing saved countless lives, and the mentioned doctor is a hero of the modern age.
The killer bug
I wish I could say that nobody had to die in order for us, software developers, to learn a similar lesson, about the importance of writing tests, but it already happened with the infamous “killer bug” in computer-controlled radiation therapy machine Therac-25, which caused it to malfunction and overdose at least 6 patients with massive (and in some cases even deadly) doses of radiation.
If Therac-25’s software was tested in a proper way, maybe this terrible series of tragedies could have been avoided.
Testing iOS apps
Situation regarding automated tests in software development community kind of looks like the late 19th century in the medical community, and this is particularly true in the iOS app development community.
Writing any kind of tests is usually an afterthought when it comes to iOS app development, especially in smaller teams. Some projects that actually have a fair amount of unit tests often totally omit UI tests.
As iOS developers, our job isn’t any different than that of other software developers, and we should stick to the same time-proven practices like other developers do, and thoroughly cover our code with tests.
Some of the tools available to us might be subpar (XCUITest, I’m looking at you!), but that doesn’t mean we can’t build on top of them and solve the problem at hand, after all, that’s what we are being paid for.
Testing at BUX
When I started working at BUX, I was assigned to a team of seasoned iOS developers, who were getting ready to start building a challenging new iOS app.
The team has already set the quality bar very high with the current BUX app, that’s been live on the App Store for several years now, and is incredibly stable, with hundreds of thousands of users and almost no crashes.
Needless to say, this stability is the product of rigorous testing procedures, that we are also applying and improving on, to the development process of the new app.
New app, new challenges
The new app that we are building is a next-gen stock trading app.
It’s designed from ground up with a mission of abstracting out the complexity of stock trading, bringing it to the people who might have never traded before, while keeping the user interface as intuitive as possible and adding a unique social component to it.
There’s a lot of overlapping realtime events such as stock quote updates, account balance updates, trade executions, social events and many others, that can happen in a split of a second and dynamically affect the user interface, sometimes multiple events at a time. Also, there’s a lot of custom flows that can be triggered from multiple points in the app.
Our solution
To reduce complexity, we have split our app into several modules/frameworks, one containing core business logic, one for making and sending http requests, another one for web socket event observation and so on.
Testing these frameworks was quite straightforward, by using unit tests, and they are 100% covered by unit tests.
The big question arose when it came down to testing the app itself.
Testing some parts of the app is quite straightforward, our logic control is decoupled from the views it supports, so we are able to easily cover most of that by using unit tests.
On the other side, testing all of the UI flows end-to-end is not so straightforward.
Testing them manually takes time, consumes manpower, and is error prone.
On the other side, automated UI testing using XCUITest isn’t the best developer experience ever.
There are some excellent iOS UI testing frameworks available out there, but we felt like most of them are not a good match for us, or are simply an overkill, so we decided to build our own simple and very minimal iOS UI testing framework.
What we are trying to do is to simulate the usage of the app in the way the user would use it, but in a controlled environment, by controlling all the data that the app receives.
In the case of unit tests this is quite simple to achieve through mocks and stubs, but in the case of UI tests it gets quite a bit more complicated because you don’t have the same level of access to the app’s code.
iOS App and its UI tests are essentially two separate processes and the only way for them to communicate is by passing some arguments at the start of the tests through XCUIApplication
’s launchArguments
variable.
There are some 3rd party solutions that enable passing data between the app and the UI test target even after they are already running, but we felt this was too hacky and error prone, so we built our framework around this constraint.
Page Object Model
Our solution is based on the Page Object Model (great explanation by Martin Fowler here) pattern in combination with stubbed HTTP responses, mocked web socket events and a mechanism that allows us to have blocking, synchronous assertions.
We set the stubbed HTTP responses at the beginning of every test case, this is achieved through the use of the amazing OHHTTPStubs framework. For the web socket event mocking, things weren’t as easy to do, so we had to create a small web server running inside the UI test target that the app connects to when UI tests start, and then we can emit the events directly from inside the UI test target accordingly to our needs. Blocking synchronous asserts are achieved through pre-existing XCUITest
methods.
In this article I will only cover the component related to POM in details, and the other 3 components of our UI test system will be covered in a future article.
Every screen has its own page, and can also have sub-pages, eg. a screen with a table view will be a page of its own while containing as many sub-pages as the number of cells it contains, and every page will have some degree of interaction available to it (tap, swipe, detect if it’s on screen, etc.).
A Page
is just a simple wrapper around a XCUIElement
:
In turn, Page is wrapped inside the UIElementPage which provides access to particular UI elements:
You can also see the examples of accessing different types of elements.
UIElement
is a simple wrapper protocol around the accessibilityIdentifier
:
In order for a view to be easily added to the accessibility hierarchy, we devised a simple protocol, UITestablePage
:
UITestablePage
also contains a method for making sequential types of views (eg. table cells) testable.
All our UI elements are organised as an enum UIElements
with sub-enums for pages, cells, dialogs, alerts, etc.
Some examples of different types of pages that we have in our app:
As you can see, a page usually has a root, which is basically the view of the view controller itself (or something else in case of a different page), and some other elements, such as buttons, tables, etc.
On the view controller’s side, utilisation of this looks like this:
First we set the adequate UIElements
value for the UIElementType
, then we write a helper method makeViewTestable
that contains calls to methods of UITestablePage
protocol that will register the elements of our view as elements of the page.
We are going to use the newly create page in SearchResultsTests
:
Here, you can see the usage of all the previously mentioned assert methods, and utilisation of the page itself. Basically this test replicates actions that a user can do on this screen, by doing a simple search, and clicking on a tag, and then asserting that the returned values are equal to expected values (that we set by our stubbing mechanism) and finally clears the text from the search bar and asserts that there are no results after that.
It’s a simple yet illustrative example and it shows the whole principle behind our POM based UI testing system.
Pages and tests can be more or less complicated, depending on the use case, and the granularity required.
Because most of the app logic is covered by unit tests, we mostly limit our UI tests to “happy flows”, so that if we break them, our CI system will automatically notify us.
Conclusion
POM enables us to abstract out most of the complexity that XCUITest brings to the table. By implementing it in our UI tests, we are able to have a simple and maintainable system that’s easy to use and further extend for anything that we might require in future.