How to Implement iOS UI Testing
During their developer careers, coders may encounter a Wirex fintech app that combines traditional money and cryptocurrencies onto one platform.
For us in the know, it is crucial to understand what’s going on with the app from the user’s perspective; Especially when 4 million active clients trust it with their money.
While a unit test checks one particular place (scene/function/module), end-to-end tests check the whole flow that the user goes through.
User Interface (UI) Tests are a convenient tool that can be used for this purpose. They launch an app and start interacting with all the visible elements going from one screen to another the same way as the user does.
Pros and Cons of Adding UI Tests
Advantages of UI testing:
- It helps to check UI functionality and find critical bugs
- Combined with Unit tests, UI tests can give us maximum code coverage
- It checks the app from a user’s perspective
- UI tests can be shown to customers to explain the necessity of testing
Concerns Regarding UI testing:
- Harder to build: Building a UI Test scenario requires preparation of the UI Elements and takes considerable time to create all the dependencies between our screen flows
- Longer running time: Each test takes about 13–34 seconds to run in total, all 21 UI tests take 8 minutes, whereas all our 42 Unit tests only take 2 seconds
- Harder to fix and maintain: Detecting the issue is a good thing, but when you want to catch a bug that’s far away from the code, it’s much harder to fix. When a unit test fails, it shows you the exact way it was broken. And yes, all changes in the UI require us to modify the UI test as well
Page Object Pattern
Once we started to write UI tests, one thing became obvious — a vast usage of string constants will clutter the code and make it difficult to maintain.
All components visible on the screen are represented as XCUIElement objects and the most common way to identify them is to use string as an identifier.
Page Object pattern is an effective solution for this problem. This is the description of our implementation.
Every screen is represented by one PageObject and every PageObject conforms to Page protocol:
This is how the PageObject looks:
Pay attention to the view = app.otherElements[“Login_Scene”] line. Unlike buttons, images, or text fields, the main view should have an explicitly set identifier.
We set it in the UIViewController of every scene, in the viewDidLoad method:
Another thing to mention about PageObject is the return type of every function — it is either Self or another PageObject.
As a result, we will be able to chain our methods. Here is what the resulting test may look like:
So it’s really quite simple, isn’t it?
We can chain our screens to create concise, readable UI Tests that are also easier to maintain.
We can reuse these Page Objects for different flows, we want to check in our UI Test, and if we make some changes in our UI, we only need to fix it in one place.
If you want to start writing UITests in your project, start by creating Page Objects. It will save you a lot of time and mental resources in the future.
Communication With API
Should you use a real server or a mocked one?
The second problem we’ve faced was the UI test’s communication with the backend API. We had to choose either to use our development server or to create mocks that imitate API requests.
We decided to implement a mock service because of the following reasons:
- We didn’t have a dedicated server that could be used for running tests only
- Our existing development servers had their state updated and often changed
- A server could be off or it could be used to test a new API, and some APIs might be broken at the time
- Supporting a dedicated server in an up-to-date state would require more time to communicate with the backend team
- The network request execution takes time on a real server, but with mocks, we receive the response almost instantly
We created an entity called MockServer that basically contained one function:
It accepts the NetworkRequest object, and closure is used for passing the request’s response.
In our case, NetworkRequest is a simple structure containing all the necessary data to make requests (URL, HTTP method, parameters, body, etc.) and conforms to Equatable protocol (to be able to use it in the switch statement).
This function contains the logic that decides whether the request should return a successful response or error:
Mocked data is passed like this sendSuccess(ConfirmLoginMock.response). The mock itself is just converted to the data JSON string:
All that is left to do is to inject the handleRequest function into the app’s network layer. In our project, we have an entity called NetworkManager that has a single point for all incoming requests. It accepts the same parameters as the above-described function:
Any request that will be passed to it will be either mocked or sent to an actual server.
We use a constant isRunningUITests to detect whether to run tests or not. And since we’re not able to pass the data between the main project and UI tests directly (UI tests are run in isolation), we need to use the launch arguments of the app.
We can do it in two steps. The first is to set an argument before the start of the UI test:
The second step is getting this argument somewhere in the main project.
- Make sure you clear your local persistent storage, User Defaults, local caches, or any temporary data that influences your app behavior before the UI test is run
- Every test needs to start from the same app state because it may still succeed when run independently but may fail when run together with the rest of the tests if they share the same state
- Double-check your mock data, it may save you a lot of time and effort. We used the raw JSON data for the mocks. We always checked that it was a valid JSON before passing it into the codebase
- Keep your tests code clean. Although it’s not a production code, you may need to return to it in the future — if it’s a mess, it will be hard to work on.