Why mocking in iOS tests may not stop network and DB activity entirely
Tags: swift, ios, testing, learning
Published: 2019-08-13 11:10:50

---
title: Why mocking in iOS tests may not stop network and DB activity entirely
published: true
tags: swift, ios, testing, learning
canonical_url: https://www.essentialdeveloper.com/articles/why-mocking-in-ios-tests-may-not-stop-network-and-db-activity-entirely
cover_image: https://thepracticaldev.s3.amazonaws.com/i/jyelp1i9sel7kc4uewzz.png
---

An excellent iOS test suite is [fast, reliable, precise, and reproducible](https://www.essentialdeveloper.com/articles/why-mocking-in-ios-tests-may-not-stop-network-and-db-activity-entirely). A common problem that makes automated testing in iOS slow and flaky is the presence of unexpected side-effects and artifacts during the execution of unit tests.

For example, unexpected state/artifacts will make:

- Some tests fail after running the app
- Some tests fail after an unfinished test run
  - Even if you clean up state in `tearDown`, if somehow the test suite does not finish properly (e.g., breakpoint or manually stopped), `tearDown` may never be executed (so the state is never cleaned)
- Some tests fail due to temporal coupling when running:
  - in random order
  - in isolation
  - the whole test suite

The examples above will make your iOS test suite slow, unreliable, inaccurate, and irreproducible. That’s why running real network requests and/or persisting state to disk during unit tests is undesired.

To make your iOS test suite fast, reliable, precise, and reproducible, every test run should start in a clean state and end in a clean state (no side-effects/artifacts left behind).

To solve the problem, iOS developers will rightly replace network clients and databases with some kind of test double. However, that may not solve the problem entirely.

Here’s a common question we receive:

_“I’m mocking my HTTP APIClient, but when running my tests, I can still see network requests being fired in the logs. It slows down my tests, and sometimes I get unexpected errors._

_For example, if I run my app and login (e.g., manually or on a UI Test), I get a bunch of failures and network requests in my unit test target because the logged-in user state is persisted and we fire a bunch of requests in the AppDelegate to update the logged-in user data._

_How can this happen if I’m mocking my HTTP APIClient in the tests?_

_I can clean the simulator before every run but that also really slows down my test process (a reset takes several seconds or even minutes)._

_What am I doing wrong, and how can I solve this?”_

The reason real network requests will be fired when running the test suite (even though the HTTP `APIClient` is being mocked) is that there is probably a Host Application attached to the test target.

If your test target has a Host Application, every time you run your tests, the Application will also run with them. For instance, if you watch your simulator while running tests, you’ll see the initial UI getting rendered depending on the state of the app.

So the `AppDelegate` is being instantiated, and it triggers the API requests through the real `APIClient` like it would if you just run the Application. Such behavior may be desired on an End-to-End UI test target, but not in an Isolated/Unit test target.

If that’s your case, instead of tweaking Xcode’s parameters to clean the startup state or blaming your app’s architecture, consider the following solutions (in order of our preference):

1. Configure the tests to run without a Host Application.
2. Create a new Test Target for Application-independent tests without a Host Application.
3. Alter the app's entry point by creating a `main.swift` file without instantiating the Application's delegate.

## 1. Run your tests without a Host Application

In our experience, the best and cleanest way to decouple your tests from a Host Application is to move your Host-Application-independent code (the majority of code you write!) into frameworks.

Framework targets do not require a Host Application and won’t have one by default. As added benefits, decomposing your Application into frameworks will help you create modular systems that are easier to maintain, extend, test, replace, reuse, and independently develop and deploy.

Alternatively, you can remove the Host Application from any existing test target, as shown below.

In Xcode, in the General tab of a Test Target, you can change the Host Application option to "none." By doing so, the tests will run without a running Application. 

 ![host_application_none.png](https://thepracticaldev.s3.amazonaws.com/i/xy8gyq4363g607mr4ct3.png)

Removing the Host Application also makes the tests faster to boot, and there's no need to make any changes to the production target (such as checking for “IS_TESTING” launch arguments). 

However, if the components you want to test reside in an *Application target*, you won’t have access to them in the test target:

![](https://thepracticaldev.s3.amazonaws.com/i/qu9so8fm1af2wh0qynpm.png)

If you don’t want to move your code from an *Application target* into *frameworks*, you’ll have to add your components into the test target manually. You can do so by ticking the checkbox in the target membership pane:

![](https://thepracticaldev.s3.amazonaws.com/i/1e085pbelfzs6znisthz.png)

Note that, if your components or tests depend on a running `UIApplication`, this solution won't cut it for you. For example, the tests will fail if they're testing any component that references `UIApplication.shared` directly because the shared `UIApplication` instance won’t exist without a Host Application.

But that’s a good thing as it helps you to avoid bad practices such as accessing implicit dependencies, mutable global state, and singletons like the `UIApplication.shared` throughout the Application and use proper Dependency Injection instead.

## 2. Create a new Test Target without a Host Application

If you're working on a test target with too many entangled dependencies on a running Application, then the cost of making the tests run without a Host Application might be too high. If that’s the case, you can take small steps instead of a significant refactoring. 

In Xcode, create a new test target and move all the Application-independent tests to a faster and more reliable Isolated/Unit test target without a Host Application. By doing so, the Application won't load when you run those tests independently. 

Gradually, make your way towards the first solution by decoupling as many components as you can from a Host Application and moving them to the new test target.

*Make sure to run all test targets as part of your Continuous Integration pipeline before merging code into the master branch.*


## 3. Replace your AppDelegate class when running tests

The third option is a complement of the approaches above. While you can’t fully decouple your test target from a Host Application or if you want to prevent your Application from creating the real `AppDelegate`, you can replace the `AppDelegate` class when running tests.

To replace the `AppDelegate`, you need to create a custom starting point for your Application. 

*Regardless if you're executing a test suite or running your iOS app, every execution has a starting point.*


Historically, in Objective-C, the main entry point for an iOS app was the main function defined in the `main.m` file, where you would create the `UIApplication` instance with an `AppDelegate` class.

{% gist https://gist.github.com/caiozullo/532c8e24ef4b4fed7198cee8ab5651ad.js %}

However, in Swift, this code is "autogenerated" for you while using the `@UIApplicationMain` attribute above the `AppDelegate` class declaration.

{% gist https://gist.github.com/caiozullo/2fd1c7ba3a24fefcf26e4e8923ce11d5.js %}

The `@UIApplicationMain` attribute denotes that the `AppDelegate` is the Application's delegate. When you run your tests with a Host Application, you’re also running your Application with the default `AppDelegate`, which might execute network requests, analytics events, database side-effects, and a lot of other unnecessary work.

To bypass the default main entry point, you can **remove** the `@UIApplicationMain` attribute from your `AppDelegate` subclass and create a new file called `main.swift` at the top level of your project.

In the custom `main.swift`, similar to the old Objective-C `main.m`, you must explicitly call the `UIApplicationMain` function passing a delegate class:

{% gist https://gist.github.com/caiozullo/ea70a4d9d8326d589c455a91d47ef706.js %}

The following code snippet below shows what is required to add in the `main.swift file` to:

- Start a `UIApplication` without a `UIApplicationDelegate` when testing
- Start a `UIApplication` with a `UIApplicationDelegate` in production

{% gist https://gist.github.com/caiozullo/64a8ad340408faeec4e3d4196082b2ac.js %}

Notice that the `func delegateClassName()` function checks if the Swift runtime can find the `XCTestCase` class. `XCTestCase` is only available when running tests, so it can't be loaded in production.

The return value of the `delegateClassName()` is being used as the delegate class name argument in the `UIApplicationMain` function, which is the main entry point of the app.

If the Swift runtime can't find the `XCTestCase` class, then the name of your `AppDelegate` subclass will be returned, otherwise, return `nil`.

Thus, if you're running the tests, the Application won't instantiate the real `AppDelegate` as the delegate class name value is `nil`.

Alternatively, you could create a custom `UIApplicationDelegate` in your test target and use it instead of returning a `nil` delegate class name:

{% gist https://gist.github.com/caiozullo/b7543a3bdb055feb1b9016f4196e4302.js %}

## Solutions to avoid

There are other popular solutions such as adding launch arguments or compile-time conditions such as `#if TESTING` clauses but, in our experience, they usually do more harm than help.

We don't recommend launch arguments or compile-time checks as they can quickly spread throughout the production codebase and become unmaintainable, e.g., creating many different execution paths when running your app for tests or production. Ideally, there should be no test execution path in your production code, making it easier and safer to develop and maintain.

## Summary

To create a fast, reliable, precise, and reproducible iOS test suite, it’s essential to eliminate unexpected/unnecessary side-effects and artifacts during the execution of unit tests. Running unit tests with a Host Application is a common source of such problems.

In our experience, the best strategy is to develop and maintain a modular codebase composed of frameworks/packages instead of developing your whole project in a single Application target.

One of the advantages of the modular approach is that this problem becomes almost non-existent as frameworks don't require a Host Application.

Finally, note that there are cases where you would want to run an Application while executing tests. For example, a UI Test target needs to run with a real Application. There's nothing wrong with that. However, if you don't need a running Application, it's better to avoid running one.