How to use Vapor Server to write stronger UI tests in Swift

Even if I usually stay focus on the customer facing side of mobile development, I like the idea of writing backend api with all the security that Swift includes. Starting small, why not using Swift Server for our UI Tests to mock content and be at the closest of the real app.

vapor-xcuitest

When it comes to testing in Swift, we often need to mock data to test our code and make sure it behave as expected. If it’s straight forward for unit tests, this can be harder when it comes to UI tests.

One way I learn is to inject environment variables, but I can’t imagine having more than 10 variables in it. At the same time, it would be more or more code on the client side to get the right content. The bigger the project becomes, the harder it will be to handle it.

So why not having a local server for our UI tests to keep all the data mocked at one place and align it with those expected behavior? That’s what I’m going to do today.

This post is based on a currency app I’ve used in the past to introduce MVVM pattern. I’ll use the same one to include a local server and use it for ui testing.

Swift Server Side

For this example, I’m going to use Vapor framework. There are many others frameworks, I’m sure you can adapt the coming logic if you use another one.

First, I create a Vapor project within the same area of my iOS project. It comes with default template, but I’ll stay focus on what concerns us.

$ vapor new TemplateProjectServer # create folder and project
$ cd TemplateProjectServer        # go into the folder
$ vapor xcode                     # generate xcode project

To keep it a clear visibility the whole project, I also created a workspace including the iOS app as well as the server one. I also clean the available target to keep only what interest me.

vapor-swift-xcode

Then, I created the currency model I’m going to return. The model is slightly different from the iOS app because I’m mocking only specific currencies, so I hardcoded the currency code. If your fields aren’t dynamic, you could keep the same one

import Vapor

struct CurrencyRate: Content {
    let eur: Double
    let usd: Double
    let gbp: Double
    let sgd: Double

    private enum CodingKeys: String, CodingKey {
        case eur = "EUR"
        case usd = "USD"
        case gbp = "GBP"
        case sgd = "SGD"
    }
}

struct Converter: Content {
    let base : String
    let date : String
    let rates : CurrencyRate
}

Here I’m using Content protocol from Vapor framework so that Vapor can automatically return JSON format. Content extends Swift Codable so I can also rewrite CodingKeys.

Now we only need a route to return our new object.

/// Register your application's routes here.
public func routes(_ router: Router) throws {
    // Basic "It works" example
    router.get { req in
        return "It works!"
    }

    router.get("latest") { req -> Converter in
        let currencyRate = CurrencyRate(eur: 1.15, usd: 1.29, gbp: 1.0, sgd: 1.75)
        let converter = Converter(base: "GBP", date: "2019-01-01", rates: currencyRate)
        return converter
    }
}

Here I create a route to be able to get currency rates. Although I didn’t implement any other logic (filter per currency, etc) but remember that it’s not our goal here. We only want to get some data from api to test the UI display without relying on the production server.

After building and launching our server, I can see my new rates under http://localhost:8080/latest. Our server is ready, only need to add our UI Tests.

UI Tests

Before diving into the test itself, we need to add couple things to our project. First, we need to support HTTP request coming from our localhost. By default, Apple doesn’t allow non HTTPS request, so we need to update our permissions.

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <false/>
        <key>NSExceptionDomains</key>
        <dict>
            <key>localhost</key>
            <dict>
                <key>NSIncludesSubdomains</key>
                <true/>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <true/>
            </dict>
            <!-- add more domain here -->
        </dict>
    </dict>

The second one is to be able to catch the environment variable for our tests. It allows to swap the url from production to our localhost when testing. I still keep a default value just in case.

final class CurrencyService : RequestHandler, CurrencyServiceProtocol {

    var baseUrl: String {
        return ProcessInfo.processInfo.environment["BASEURL"] ?? "https://api.exchangeratesapi.io"
    }

    var endpoint: String {
        return baseUrl + "/latest?base=GBP"
    }

Finally we can create our test and use our new testing environment


import XCTest

class VaporCurrencyUITests: XCTestCase {

    // ...

    func testExample() {
        let app = XCUIApplication()
        app.launchEnvironment = ["BASEURL" : "http://localhost:8080"]
        app.launch()
		  let tablesQuery = app.tables
        let count = tablesQuery.cells.count
        XCTAssert(count == 4)
    }
}

This test is quite simple, I make sure that with a valid request I have the same amount or data displayed. Make sure you run first the Vapor project before launching the UITests, otherwise it will fail.

Regarding our settings, we could go further and customize our url to expect specific amount of result and test multiple times, or test when the api (expectedly) failing. It gives us many many ways to improve our UI tests.

Continuous deployment

Our project is nicely setup but we still need to build and run different targets to launch server and tests. I haven’t find the right balance yet but I’m sure we could go further and automatically launch our server as soon as we launch our UI tests.

First I tried to use shell script, but as vapor keeps running, the tests were never launched. There might be another way to do that but in case you are curious to look into it.

if which vapor >/dev/null; then
    cd ./TemplateProjectServer
    vapor run
else
    echo "warning: Vapor is not installed, download Vapor first"
fi

Another way to handle this is to use external dependencies to execute shell script from within Swift code. I’m thinking about swift-sh or ShellOut but they might be others.

If you don’t run your UI tests locally but from a CI job, you might not have this issue and just run it from your CI script, fastlane or any other configuration. It probably makes on CI actually.

This whole project is available on Github under vapor branch.


In conclusion, we’ve seen how to integrate a Swift Server project into our iOS project to build stronger and more reliable UI tests. It’s also a small introduction for me to Swift Server with Vapor but I like the idea of Swift developers doing the extra mile on backend side to get better testability of the iOS app.

What do you guys think? Have you already tried Swift Server for testing purpose? What makes it great?

Happy coding

© 2023 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

Software Engineer 🇫🇷, writing about career development, mobile engineering and self-improvement

ShopBack 💰

Singapore 🇸🇬