Contents

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>localhost</key>
    <dict>
      <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

before the last two lines:

1
2
</dict>
</plist>

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:

1
2
3
4
5
6
7
func print() {
	#if HELLO_WORD
		print("Flag set")
	#else
		print("Flag not set")
	#endif
}

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:

  1. Open Build Settings of your project
  2. Search Active Compilation Conditions
  3. 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:

1
2
3
4
5
6
7
#if DEBUG
	// do something for build configuration Debug
#elseif RELEASE
	// do something for build configuration Release
#elseif TESTS
	// do something for build configuration Tests
#endif

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

1
2
3
{
    "api_url": "https://jsonplaceholder.typicode.com/"
}

Config-Test.json

1
2
3
{
    "api_url": "http://localhost:1234/"
}

and add them in our Xcode project:

Then, we must read the right JSON file depending on our build configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private func jsonFileContent() -> String? {
    var fileName: String? = nil
    #if DEBUG
        fileName = "Config-Debug"
    #elseif RELEASE
        fileName = "Config-Release"
    #elseif TESTS
        fileName = "Config-Test"
    #else
        fatalError("Config flag not found")
    #endif

    guard let file = Bundle.main.path(forResource: fileName, ofType: "json") else {
        return nil
    }
    return try! String(contentsOfFile: file)
}

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:

1
let model = Mapper<ConfigModel>().map(JSONString: jsonFileContent())

Once loaded the JSON values, we can use them to send an API request to get the users list:

1
2
3
4
5
6
func fetchUsers() -> [Users] {
	// apiUrl is the value of the config JSON file
	let url = apiUrl + "users"
	
	// send a API request to url and fetch the data
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

VERSION=2.6.0
PORT=1234

if ! [ -f "wiremock-standalone-$VERSION.jar" ];
then
   wget http://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-standalone/$VERSION/wiremock-standalone-$VERSION.jar
fi

java -jar wiremock-standalone-$VERSION.jar --port $PORT

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "request": {
        "method": "GET",
        "url": "/users"
    },
    "response": {
        "status": 200,
      	"bodyFileName": "users_list.json"
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "request": {
        "method": "GET",
        "url": "/users"
    },
    "response": {
        "status": 200,
      	"body": "Hello World!"
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
  {
    "id": 1,
    "name": "Mock User #1",
  },
  {
    "id": 2,
    "name": "Mock User #2",
  }
]

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

1
2
3
4
{
    "userId": 1,
    "userName": "User #1"
}

Response for userId 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "request": {
        "method": "POST",
        "url": "/user",
        "bodyPatterns": [{
		      "matchesJsonPath" : "$[?(@.userId == 1)]"
		    }]
    },
    "response": {
        "status": 200,
      	"bodyFileName": "users_list.json"
    }
}

Body POST request for userId 2

1
2
3
4
{
    "userId": 2,
    "userName": "User #2"
}

Response for userId 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "request": {
        "method": "POST",
        "url": "/user",
        "bodyPatterns": [{
		      "matchesJsonPath" : "$[?(@.userId == 2)]"
		    }]
    },
    "response": {
        "status": 500,
    }
}

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:

1
2
3
4
func test_UsernamesTable_DataIsLoaded() {
    let areUsernamesLoaded = XCUIApplication().tables.cells.staticTexts["Mock User #1"].exists
    XCTAssertTrue(areUsernamesLoaded)
}

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.