How to run Swift UI tests with a mock API server
Are you developing an application which uses API responses? Do you need an efficiently way to test the UI? UI testing with a mock API server is the solution to your problems.
Overview
UI testing is a great tool to test the flow of your application. It can catch regression bugs and unexpected UI behaviours.
Like the unit testing, an UI test must be written well and efficiently, since you may run it several times a day to refactor/add a new feature in your application.
In this article, I explain how to use a mock API server for your UI tests, step by step, with a sample project.
Sample Project
The project used in this article is a plain application which shows a table of usernames:
I used the architectural pattern MVVM-C. If you are not familiar with it, you can check my article about architectural patterns. Of course, you can use the pattern which you prefer, it’s a personal choice.
You can find the sample project here (it has both mock server and iOS project).
I won’t explain how to create the UITableView
, since it’s beyond the goal of this article. If you don’t know how to do it, you can check in the sample project.
Project Configuration
We must configure our project to use the mock API server for UI testing and remote API for Debug
and Release
. To achieve it, we can create a new build configuration for the tests, and read the server url from a JSON file for each build configuration.
App Transport Security Exception
Since iOS 9, your app must support App Transport Security (ATS). This feature forces your app to use just secure connections. It means that the app must communicate with remote servers using the protocol HTTPS instead of HTTP.
Since we are running the mock server locally, we don’t need a secure connection for it. To disable ATS for localhost
, we must edit the file Info.plist
of the main target.
You can open the plist file with a text editor and append:
|
|
before the last two lines:
|
|
The result in Xcode will be:
If you have some problems with the Info.plist, I suggest you to copy/paste the values from the sample project.
Build Configurations
We have to load the config file programmatically with Swift, therefore we need a way to understand when our application has been run by UI tests. There are several ways to achieve it. In this article, I use Active Compilation Conditions
. It’s a feature added in Xcode 8 and allows you to add conditional compilation flags to the Swift compiler. Therefore, if we add the flag HELLO_WORD
then we can check it programmatically:
|
|
You’ll see a realistic usage later.
Let’s start creating a new build configuration:
Then, we must edit the scheme to use the new build configuration for the tests:
Remember to set the scheme as shared. By default, the scheme is not shared, therefore the changes, which you make, remain locally in your machine. Setting shared, you allow the other users of the project to use this scheme.
Last step, we must set Active Compilation Conditions
:
- Open
Build Settings
of your project - Search
Active Compilation Conditions
- Add the following flags to the build configurations (
DEBUG
,RELEASE
,TESTS
):
You don’t need to do it also for the build settings of the targets, since they will inherit the settings of the project.
Thanks to these flags, everywhere in your Swift code, you can check:
|
|
Config Files
For Debug
and Release
, we will use the free API at jsonplaceholder.typicode.com, whereas for our UI tests we want to use our mock server, which will be running in localhost with the port 1234—if you want to change the port number, pay attention to change it also in the mock server configuration.
We can start creating three config files:
Config-Debug.json and Config-Release.json
|
|
Config-Test.json
|
|
and add them in our Xcode project:
Then, we must read the right JSON file depending on our build configuration:
|
|
The method jsonFileContent
returns the content of the right JSON. How to parse the JSON is beyond the goal of this article. You can choose whatever way you prefer. In the sample project, I use the library ObjectMapper:
|
|
Once loaded the JSON values, we can use them to send an API request to get the users list:
|
|
In this way, when this code is running with the build configuration TESTS
, apiUrl
has the value http://localhost:1234/
, therefore url
is http://localhost:1234/users
. Instead, with Debug
and Release
the value of url
is https://jsonplaceholder.typicode.com/users
.
How to send the API request and fetch the data is beyond the goal of this article, you can check the sample project if you want to do it using RxSwift. Otherwise, I suggest you to have a look at AlamofireObjectMapper, it is a library to send API requests with Alamofire and to parse the responses with ObjectMapper.
Mock Server Configuration
For the mock server, we use WireMock as standalone process, which will be running in localhost with the port number 1234.
WireMock is an HTTP mock server which allows you to map API requests, in this way you can simulate the API responses with WireMock instead of using the server in production.
Setup
The setup of WireMock is extremely easy, you can find the documentation here. If you want a script to run it easily, you can use the one I added in the sample project:
|
|
This script checks if you have the WireMock jar
file, otherwise downloads it. Finally, it runs the server with the port number 1234.
At the moment of writing, 2.6.0
is the latest version of WireMock.
Request Mappings
After the first run, WireMock creates two folders: mappings
and __files
.
Mappings
This folder contains all the WireMock responses. You can create a new response adding a new JSON file in this folder. The name of the file doesn’t matter, but I suggest you to use meaningful names since you may have thousands of files.
For the sample project, we need to add the current mapping file:
|
|
The JSON values are very straightforward. We use: method (GET, POST, DELETE, ..), url, response status (200, 404, 500, …) and the response body.
bodyFileName
is the file path where to read the response—the folder root of that file is __files
, we’ll see it later.
If you want to add the response body with a string instead of reading a file you can use the node body
:
|
|
Here you can find other values to add to your mapping file like cache, header and so on.
__files
In this folder, you can place files to download or to use in your mapping files.
For our sample project, we need this folder do add our JSON response:
|
|
At the end of the setup, our WireMock folder structure will be like this:
You can ignore index.json
. It’s just a mapping for the server Homepage to show the message WireMock is running!
.
Every time you add/edit a file in these two folders, you must restart the server.
Advanced Usages
WireMock allows you to add request matching. With this feature, you can create different responses depending on the data you send in the request. You can find the documentation here.
For example, if your app sends in the request body a JSON with the user id, you can easily match it:
Body POST request for userId 1
|
|
Response for userId 1
|
|
Body POST request for userId 2
|
|
Response for userId 2
|
|
In this way, you can login with different users in your UI tests to test different API responses.
UI Testing
Thanks to our configuration, every time we run a UI test, we use the WireMock responses. Therefore, when we load the table of usernames, you get the data from http://localhost:1234/users
. The final result is:
and we can test our table with the following UI test:
|
|
Conclusion
In this article, I shown just simple examples. WireMock is a powerful mock server, you should read the documentation thoroughly to learn all its features.
You may be wondering why I wanted to use JSON files to load the app configuration instead of using other tools. The power of this approach is that, instead of embedding in the Xcode project, you can store them in a remote server. Then, at the app startup you can send a request to this server to download the newest JSON config files. In this way, you can easily edit the config files in the server to change on the fly the configuration of your application.