Self-contained UI tests for iOS applications

We’re all familiar with
TDD, or at least write unit tests for our software,
but unit tests won’t check application state after complex UI interactions.
If you want to make sure that an application behaves correctly when users interact with it,
then you need to write UI tests.

Automated UI tests of your mobile application can help you detect problems with your code during everyday
Continuous Integration (CI)
process. It may however be hard to achieve a stable test environment if your application presents data obtained from
remote servers.

This article explains how to set up a self-contained test environment for connected iOS applications, that can be used
both in Continuous Integration and manual testing.

UI Tests

We’ll use Xcode UI Testing for UI tests.
It’s the official UI testing framework from Apple that reduces the need for explicit waits in test code.
Less explicit waiting means faster and more readable test code which is very important for test suite maintenance.
Also, as it’s the official Apple framework, we’ll hopefully avoid situations when the test framework breaks with new
Xcode releases.

Unfortunately there isn’t much official documentation for the framework, but Joe Masilotti did
a tremendous job of documenting and explaining all of the quirks.

Test target setup

First of all we need a test target. We’ll add a new UI Testing bundle to our project:

We want to run UI tests as part of CI and also allow developers to immediately see if their code changes are passing
UI tests when they run tests manually.
Thus we will not create a separate scheme for UI tests, but we’ll build and execute them as a part of Test action for
our default scheme.

To do this we have to set up our project as on the screens below:

Disabling animations

UI tests usually take a lot of time compared to simple unit tests. We want to make our tests as fast as possible as
we’ll be running them as part of CI. We’ll disable UI animations in the application when UI tests are running to speed
things up.

This can be done by setting an environment property, which we will later check at application start.
The best way to do this is to extend XCUIApplication class as it has to be done before every test:

But if we run the test, we’ll discover a nasty side-effect of our helper methods:

Error marker is placed within the helper method when the test fails.
This is not a big problem when the helper is used only once, but we’ll be using it multiple times in test methods.

Thankfully this is easy to fix. Every assertion method takes two additional parameters which tell Xcode from where in
the source file the assert comes. We’ll use those parameters to place the error marker in the test method.

We can see that the marker is correctly placed when we run the test again. We didn’t even have to change anything in our
test method!

Network data stubbing

So far we were using hardcoded test data in the test method, but the truth is that our application presents data
received from backend servers. Thus it’s possible that this data will often change. If suddenly a different product
is the first one on the product listing then the test will fail as the name or price won’t match. Moreover a server
outage will cause our tests to fail as well because we won’t receive any data at all.

How can we ensure that our tests will be server data independent and that they won’t fail if the server is experiencing
downtime? Network data stubbing can help us with that.

There’s a great, well documented, opensource project called WireMock that we can use
to serve network stubs. It not only serves but also records stubs, which is really handy if you want to mock network
communication quickly.

WireMock script

We want to run WireMock before every test run and also have the possibility to use it for manual tests and network
communication recording. It would be hard to remember the whole syntax of WireMock commands so let’s write a simple
script that would do all those things for us:

Now we’ll be able to start WireMock by simply running ./wiremock.sh and stop it by running ./wiremock.sh -k.
We can even run WireMock in record mode to record new mappings with ./wiremock.sh -r.

The script expects to find mappings and __files directories with mock files in the script directory — this can
be changed by providing -m path_to_mappings option.

Build configuration

Now that we have our script, it would be good to start WireMock before every test session and stop it afterwards.
We can achieve this by adding pre- and post-actions for Test action that will run the script with correct parameters.

Assuming that wiremock.sh is made executable and placed in WireMock directory under applicationUITests our actions
would look like this:

So now we start WireMock before every test session, but… our application is not using it. We have to configure the
project and make a small change in application code so that it connects to localhost when needed.

Let’s start with project configuration.

We want our Test action to use localhost and we also want to use localhost for manual testing when needed.
The easiest solution that fullfils both requirements is to create a new build configuration that will set a special
build flag at compilation time.

To achieve this we have to clone the Debug configuration (as this is the configuration used by Test) on project Info
screen and give the new configuration a meaningful name (e.g. Localhost).

Build configurations should look like this afterwards:

Now we have to add a new custom flag (-DLOCALHOST) for
Swift
compiler on Build Settings screen like this:

It’s time to make sure localhost is used instead of real API URL in Localhost configuration.
We have to add conditional code in the place where API URL is defined:

We’ll be sending requests to WireMock over an unencrypted connection so we need to allow arbitrary loads in Info.plist.
We only want to do this for Localhost configuration and no other so we’ll add two additional build phases to
the application target.

First one will run before actual compilation takes place and will enable arbitrary loads for Localhost:

Using data from mocks in tests

So far our test methods included hardcoded data like “Product 1” for item name. This isn’t really flexible because we
would need to change those hardcoded values every time we make a change in mocks.
It would be way better if we used the data loaded from mocks.
We can do this by creating a simple mock data parser for tests.

But first things first — let’s bundle mocks with the test bundle so we have files to read from.
The easiest way to do it is to reference __files directory in the UI test target like this:

Afterwards we’ll have a reference to the __files directory in our project structure:

This way we can easily access mock files in Xcode and they will be automatically bundled with test bundle.

We are ready to write our parser code now.

Let’s start with a simple base class for test data parsers that will load a specified mock file and deserialize it
into a dictionary at initialisation time.
We’ll also create a simple type alias called JSONDict for readability purposes as it’s easier to use in code than
[String: AnyObject]