Mock Network Requests in UITests
By: Jan Olbrich
Tags: iOS, Software Development, Testing, Swift, Software Engineering

Networking is a constant issue iOS developers have to face. There is nearly no app, which doesn't require it. In tests we need reproduceble and stable results, and network doesn't guarantee this. Ideally, we want to throw our Mac into a dark cave 100m below surface and run our automated tests. Having no network connection shouldn't be the reason for failing tests. Since the apps depend on getting data from backends, this needs some work on our side.

In Stub network calls in Unit-Tests we made sure to control our unit-tests. As a nice side effect it also offered the dark cave option. So there is nothing more to do. To recap, OHHTTPStubs was used to return data on NSURLRequest level. Sadly this trick doesn't work for any UITest framework which doesn't share the process with the app (EarlGrey could work ;)).

Continue using OHHTTPStubs for UI-Tests

Your first thought might be: "Why not use OHHTTPStubs within the app?" This is possible but there are a few reasons I dislike it:

It is hard to control what needs to be returned. It changes a lot of logic within the App. It adds (testing) code, which doesn't belong in the production code.

Control the backend

When writing UI-tests you need to consider what you are testing. Do you want to test solely your app, or do you also want to test your system? The companies I've worked for had a tendency towards testing the entire product. This was just a crutch as they didn't have the confidence in their backends working correctly.

Testing your system sounds nice, but an error doesn't really tell you, what is broken. You'll have to look everywhere. So I advice you to use your UI-tests for your app. To achieve this, you have to create your own backend for the tests. It has to run on the same machine, so no network dependencies exist. Setting up your entire backend on one machine sounds like overkill so let's skip this. Instead, I suggest creating a mock backend server on which you can control the responses you need for your test.

Basic structure of your Mock Backend

What are our requirements for our test-backend?

We can control, what it returns It runs outside our app Minimal changes to our production code

These can only be fulfilled by writing our own backend. The most basic idea would be to have a program with a dictionary for every request type. This dictionary maps paths to responses. So whenever we call 'GET /hello' it will return "200 OK - world".

In case you prefer to write this backend as a stand alone app, you can do it. Maybe have some kind of network API like:

"/getRoute?path=hello&result=world"

This would be quite easy to implement. Just have a dictionary [String:String] per request type and map the path towards the response.

I see a few issues with such a stand-alone backend:

You need to make sure it starts correctly before the UI-tests run

Changing paths require a network client

In my opinion unnecessary complexity

It's probably in a language not all iOS developer can use

But sometimes these issues are less of a problem or even an advantage. Imagine not being able to use XCUITest and instead use some kind of external tool. You will have to create and control it from outside of the testing code.

Thanks to a lot of diligent developers we can use Swift not only for apps, but also on the backend. So why not use it and make the iOS developers write their backend in the same language as the app? The advantages are:

No new language for developers

All app developers can work on it if necessary

Started from within XCUITest you can configure it within the code

Setting up the Backend

To implement our backend we will use Swifter. It is a tiny HTTP engine in which you can create quite fast a fully functional webserver. Interestingly it behaves quite similar to our internal structure described above.

To configure responses you simply set them internally via:

server.GET[path] = HttpResponse.ok(response)

Within a UI-test we can start the server, configure it and only afterwards start the app. Of course it offers the possibility to dynamically switch our responses in the middle of the test. Let's look at some pseudo code:

func test1() {

mockServer.configure()

mockServer.launch()

app.launch()

executeTest()

}

One nice advantage is, for the server to only exist while you are testing. In case macOS requests you to allow network connections (the usual firewall screen) you can either create a rule and permanently allow connections, deactivate the firewall (I wouldn't advice it), or just ignore the message. Interestingly while the dialog is open and you haven't declined it yet, the connections are still being established.

App

The only open question left is the app. As the server is running locally you will need to add a new environment to your app, which redirects all network requests to the server. Yuri Chukhlib has written a great post about handling app environments. So go there before continuing, in case you don't know how to handle these.

To use our mockserver we have to add another environment. This has a base url of "127.0.0.1" as it should connect to our locally running server. Depending on your release process you might not want to recompile your app, simply for testing purposes. So you somehow need to change the environment the app is working on. Some apps I've worked on have a secret gesture to get into a debug menu. Within this you were able to change the environment. Most often developers shy away from this option, as they are afraid of users finding them (it's just a matter of finding a good place to hide.. but still). So instead of doing this, we can pass launch arguments to the app.

This is one more thing you can do within the testing code:

app.launchArguments.append("USE_MOCK_SERVER")

Within the app we can check for this launch argument:

if ProcessInfo.processInfo.arguments.contains("USE_MOCK_SERVER") {

}

This is the only change we have to do within our production code. You can use this technique for other necessary changes within the app, but don't overuse it. I don't advice you to add different states within the app (such as "let's use this parameter to make the app think it's logged in"). Instead let the tests run a little bit longer and have less code in production. The more code there is, the more complicated it gets. So keep it as simple as possible.

Conclusion

With this mock server introduced we can run our tests in the dark cave. Interestingly enough, it helped my team to understand our network requests better. Suddenly we had to know, which calls were done at which time. It also sped up our testing time by quite a margin. All in all, I can only advice you to stub your entire network. It improves your testing stability and speed.

Next: Screen Objects

Previous: Automated UI-testing for iOS Apps