Painless UI Testing in iOS (Part 1) - Mocking the Network

Table of Contents

Let’s be realistic, UI Testing is a pain. For everybody. And with everybody, I include both Android and iOS developers. The main reason for it is that we’ve all come to this point when one-tenth of the UI Tests fail, randomly, for no definite reason. Could be the CI Virtual Machine which was slower than usual, or a small latency in the emulator… Don’t get me started with execution time… We’ve all been through that.
Well, guess what!

The time has come to make UI Testing great again.

🎬 Hi there, I’m Jean!

In this article, I am going to walk you through 3 tips to make your UI Tests faster and more reliable! 💪
By the way, I am a loyal XCUITest soldier so, XCTest it is gonna be! ✊
Of course, those tips will also be valid for any UI Testing framework. 👍

Mocking the Network


There is no way around it, if you want to avoid flakiness during UI Testing, you have to mock the network.

Now, in iOS, there is no straightforward way of doing it. 🤔
We have to inject mocked data to the host executable at runtime, through an environment variable. 💉
But first, we need to prepare our Network layer for this kind of mocking and there are several ways of doing it. Personally, I like to protocolize the URLSession to inject a mocked implementation in my NetworkManager! 🤓
See my article Lightweight Design Patterns in iOS (Part 1) - Observer for more details on how I like to build my Network layer! 😃

Alright, alright, I’m gonna explain it to you! 😜
But first, let’s create a protocol (URLSessionProtocol) abstracting Foundation’s URLSession. 👌

import Foundation

protocol URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol
}

extension URLSession: URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol {
        return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask
    }
}

Now, as you can see, we also need a protocol for the URLSessionDataTask, so we can also make a mock implementation of the data task! 😃
Right on, let’s write our URLSessionDataTaskProtocol! ✍️

import Foundation

protocol URLSessionDataTaskProtocol {
    func resume()
    func cancel()
    func suspend()
}

extension URLSessionDataTask: URLSessionDataTaskProtocol {}

OK! 👌 That’s all the abstraction we need, for now at least. 😉
At this time, we can already inject it in our NetworkManager and see how it plays out! 👍

final class NetworkManager {

    private var session: URLSessionProtocol
    ...

    init(session: URLSessionProtocol = URLSession(configuration: .default), ...) {
        self.session = session
        ...
    }
    ...
    
    func dataTask(with urlRequest: URLRequest, ...) {
        session.dataTask(with: urlRequest) { ...
            ...
        }
        ...
    }
    ...
}

The really good thing about it is that, there is no “test code” in the NetworkManager, it almost looks unchanged. 💯
All we did was abstract the session with a protocol to allow a different implementation of it! 🚀

Before we get to the mocking part, we first need to define the way we are going to mock the responses. 📄
Let’s start with the Response struct! 👍

import Foundation

struct Response: Codable {
    var file: File?
    var error: NetworkError?
    var dataTask: URLSessionDataTaskMock

    init(_ file: File? = nil, error: NetworkError? = nil, dataTask: URLSessionDataTaskMock = URLSessionDataTaskMock()) {
        self.file = file
        self.error = error
        self.dataTask = dataTask
    }
}

As defined above, a Response can take a File (JSON or image) an Error and a URLSessionDataTaskMock.

Before going any further, let’s see what a File is actually about. 👀

import Foundation

final class File: Codable {
    var name: String
    var `extension`: Extension

    enum Extension: String, Codable {
        case json
        case jpg
    }

    init(_ name: String, _ extension: Extension) {
        self.name = name
        self.`extension` = `extension`
    }

    var data: Data? {
        guard
            let url = Bundle(for: type(of: self)).url(forResource: name, withExtension: `extension`.rawValue),
            let data = try? Data(contentsOf: url) else {
                return nil
        }

        return data
    }
}

Thus, the File is the entity where we encode the responses to be injected in the launch environment, at runtime. 📁

Nice! 😎
Now we have the mock Responses as well as the protocols ready, mocking is very easy. 😃
All we need is making our own implementation of URLSessionProtocol and URLSessionDataTaskProtocol for testing purposes! 💯

But first, let’s define our Request enum! ✅

enum Request: Hashable {
   case fetchSomething
   
   var url: String {
      switch self {
      case .fetchSomething:
         return "https://api.com/something"
      }
    }

    var method: HTTPMethod {
        switch self {
        case .fetchSomething
            return .get
        }
    }

    var parameters: Parameters {
       switch self {
       case .fetchSomething:
          return .url(["api-key": "abcd1234"])
       }
    }
}

Now, because we are going to use the Request as key to our responses Dictionary, we are going to map it to an absolute URL. 🗺

For this purpose, let’s first create an extension on Parameters for getting a String representation of a query out of it. 👌

extension Parameters {

    var query: String? {
        switch self {
        case .url(let url):
            guard !url.isEmpty else {
                return nil
            }

            return url
                .map { key, value in
                    guard let value = value else {
                        return key
                    }
                    return "\(key)=\(value)"
                }.joined(separator: "&")
        default:
            return nil
        }
    }
}

And now we’ll create an extension on Request to get a String representation of an absolute URL from it. 👍

extension Request {

    var absoluteUrl: String {
        guard let query = parameters.query else {
            return url
        }

        return "\(url)?\(query)"
    }
}

Alright, we are good to go! 🎉
Let’s get started with the URLSessionMock then! 💪
All it needs is a mutable Dictionary of Requests (keys) and Responses (values) as well as conforming to our URLSessionProtocol. 👌
The Responses Array inside the Dictionary will behave like a Queue (FIFO — First In First Out), meaning that they will be consumed just like a real network would! 😮 Simple! 🙌

import Foundation

final class URLSessionMock: Codable {
    private(set) var responses: [String: [Response]]

    init(responses: [Request: [Response]] = [:]) {
        self.responses = Dictionary( uniqueKeysWithValues:
            responses.map { request, responses in
                return (key: request.absoluteUrl, value: responses.reversed())
            }
        )
    }
}

extension URLSessionMock: URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol {
        guard
            let url = request.url?.absoluteString,
            let response = responses[url]?.popLast() else {

                completionHandler(nil, nil, NetworkError.invalidRequest)
                return URLSessionDataTaskMock()
        }

        completionHandler(response.file?.data, nil, response.error)

        return response.dataTask
    }
}

With this implementation, we have complete control over the requests being made. 👮
We can decide which Response to return depending on the Request and even stub on the data task! 🤯
Speaking of which, here is the mock implementation of the URLSessionDataTaskProtocol. 😉

final class URLSessionDataTaskMock: Codable {
    private(set) var isResumed = false
}

extension URLSessionDataTaskMock: URLSessionDataTaskProtocol {
    func cancel() {
        isResumed = false
    }

    func suspend() {
        isResumed = false
    }

    func resume() {
        isResumed = true
    }
}

Almost there! ☝️
Before writing our tests, let’s add some helper functions/variables! 👍

To avoid making use of hardcoded strings, let’s create an Identifiable protocol which will provide an identifier to any class that conforms to it! 💪
That way, when injecting the URLSessionMock in the launch environment dictionary, we can use the identifier variable as the key. 🔑

import Foundation

protocol Identifiable {
    static var identifier: String { get }
}

extension Identifiable {
    static var identifier: String {
        return String(describing: self)
    }
}

extension URLSessionMock: Identifiable {}

And finally, here are some small extension functions/variables for encoding/decoding our mock from the environment! 💯

Let’s start with an extension on ProcessInfo to decode any Identifiable

import Foundation

extension ProcessInfo {
    
    func decode<T: Identifiable & Decodable>(_: T.Type) -> T? {
        guard
            let environment = environment[T.identifier],
            let codable = T.decode(from: environment) else {
                return nil
        }

        return codable
    }
}

Then, make an extension on Decodable to decode from any String

import Foundation

extension Decodable {
    
    static func decode(from json: String) -> Self? {
        guard let data = json.data(using: .utf8) else {
            return nil
        }

        return try? JSONDecoder().decode(self, from: data)
    }
}

Lastly, write an extension on Encodable to encode to either String or Data! ✅

import Foundation

extension Encodable {

    var json: String? {
        guard let data = data else {
            return nil
        }

        return String(data: data, encoding: .utf8)
    }

    var data: Data? {
        return try? JSONEncoder().encode(self)
    }
}

Finally! 🏁
We are done with preparations! 🎉

Now, we need to inject the URLSessionMock in the launch environment so it can be received in the AppDelegate! 💉
Let’s create a nice extension function on XCUIApplication to help us achieve that. 💪

import XCTest

extension XCUIApplication {

    func launch(with sessionMock: URLSessionMock = URLSessionMock()) {
        launchEnvironment[URLSessionMock.identifier] = sessionMock.json
        launch()
    }
}

And last but not least, the AppDelegate is now able to decode the URLSessionMock from the ProcessInfo and pass it to the NetworkManager! 👌
Yes, that’s all. 😎

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        let session: URLSessionProtocol = ProcessInfo.processInfo.decode(URLSessionMock.self) ?? URLSession(configuration: .default)
        let networkManager = NetworkManager(session: session)
        ...

        return true
    }
}

All set! ✊
Now, look how simple and elegant UI testing is! 😮

import XCTest

final class SomethingUITest: XCTestCase {
    private lazy var app = XCUIApplication()

    override func setUp() {
        super.setUp()
        ...
    }

    func testSomething() {
        let sessionMock = URLSessionMock(
            responses: [
                .fetchSomething: [
                    Response(File("something_response", .json)),
                    Response(error: .invalidResponse)
                ]
            ]
        )

        app.launch(with: sessionMock)
        ...
    }
    
    ...
}

Boom! 💥
The Network layer is now completely under our control and can be mocked with the snap of a finger! 👍
We can specify at runtime, on a per-test basis, which Responses to return, just by defining a Dictionary of Requests and Responses. 🗃

Was it worth it? Did that help you overcome the problems of UI Testing, starting with Mocking the Network? Are you now feeling more confident about writing UI Tests?

If so, follow me on Twitter, I’ll be happy to answer any of your questions and you’ll be the first one to know when a new article comes out! 👌

If you’d like to see a real-life example of an app doing extensive UI Testing, don’t hesitate to check out my Github repo! 📦

See you next week, for Part 2 of my series Painless UI Testing in iOS!

Bye Bye! 👋