Faking solution for Swift testing

David Schuppa
Emarsys Craftlab
Published in
8 min readMar 8, 2023

--

This article was co-authored by László Őri.

The team

We are a team of four, using extreme programming methodology with practices like collective ownership, pair programming, TDD development, continuous integration, etc.

The product

We’re developing the Android and iOS EmarsysSDK, which is the unified version of the former separate SDKs for mobile marketing and product recommendations. This SDK provides an API for our customers’ mobile applications to be able to communicate with our backend in favor of enhancing user engagement and customer retention, by providing real-time personalized experience and product recommendations. More information about the product is available here.

The task

Our SDK was written in Objective-C, but we decided to rewrite and redesign it in Swift for several reasons.

To mention a few of them:

  • Over the years we had to implement some complex logics in the SDK, because the foundations of the architecture weren’t prepared for everything. More and more customers started to use the SDK and the product grew, and sometimes it was hard to keep maintainability and backward compatibility, while being able to add new features and improvements to the code.
  • Apple introduced Async/Await in Swift 5.5, which made the whole team excited. The idea of moving away from callbacks/closures and introducing a more reactive way to the SDK was very tempting. It is really clear that this is the preferred way in mobile development now, everyone is heading in this direction.
  • Our codebase is written in Objective-C and if we want to be honest despite the fact that how much we like the language, Swift is much easier to pick up and it would help contributions and new members to pick up the tasks.

The challenge

Like many other developer teams, we also use unit tests to provide a safety net under our codebase to ensure business logic reliability. One common practice is to use mocks as class dependencies to be able to focus the scope of the tests on one particular part of the logic, excluding the external logic added by the injected dependencies. The problem is that mocking is not available in Swift.

Swift is a strongly typed language with only introspection as reflection in it.

The lack of runtime modification ability means that no real working mocking libraries exist.

After we spent a lot of time investigating and trying out many solutions, we realized that these are based on build time generation, and all of them forced adding something to the production code, like comments or conforming to extra protocols.

Some of them were usable, but we ran into some limitations quite early when we started using them, such as not fully supported async functions or some problems with @Actor handling. With manual adjustments in the generated “mocks” some of them were usable, but this just simply doesn’t scale well if we think about it. What works with 2–3 classes won’t work with a huge codebase, and imagine editing the generated mocks manually to be able using them. It would take a lot of time and effort.
So based on these findings, we could not fully commit to a 3rd party library and do not know when these kinds of issues will be resolved.

As a plan B, the common approach is to use fakes instead of mocks. With correct code organizing and protocol-oriented programming, we are able to create fake instances for classes and structs that could work as we would like to and be able to change the behavior of the original logic, but it has a big overhead and is not as convenient as mocking. So we started to think about what we could do with this issue.

The solution

We came up with a hybrid solution, using polymorphism, generics, introspection and type checking, we created a protocol and an extension for that, which allows us to inject the behavior of a function from the test, and make it easily reusable and extendable. It has an overhead compared to mocking solutions, but much less than fake instance creation from scratch for every class/struct.

Our solution is not finished yet, it is continuously evolving as we use it and we try to come up with new ideas every time facing a new issue.

This is the current state of the protocol:

import Foundation
//1
typealias FakedFunction = (_ invocationCount: Int,
_ params: [FakeValueWrapper<Any?>]) throws -> (Any?)

protocol Faked {
//2
var instanceId: String { get }
//3
func when(_ fnName: String,
function: @escaping FakedFunction)
func when(_ fnKeyPath: KeyPath<Self, String>,
function: @escaping FakedFunction)
//4
func handleCall<ReturnType>(_ fnName: String,
params: Any?…) throws -> ReturnType
func handleCall<ReturnType>(_ fnKeyPath: KeyPath<Self, String>,
params: Any?…) throws -> ReturnType
func handleCall<ReturnType>(_ fnName: String) throws -> ReturnType
func handleCall<ReturnType>(_ fnKeyPath: KeyPath<Self, String>) throws -> ReturnType
//5
func props() -> [String: Any?]
//6
func assertProp<T: Equatable>(_ propName: String,
expectedValue: T) throws -> Bool
//7
func tearDown()
}
  1. We declared a typealias for a needed closure where we will inject the expected behavior to the faked method/function. It has the count of the invocations and all the parameters of the call. The FakeValueWrapper makes it extra convenient, so we don’t have to cast the parameters when we are making assertions.
  2. instanceId — for the injected functions we use a static function holder and somehow we have to identify the class or struct instance where it’s connected, since we want to use structs as well we can’t use simply the Swift Standard Library’s ObjectIdentifier, so we expect that the instances which are conforming to the Faked protocol to implement this and make sure this value is unique. Sadly since extensions can’t contain stored properties we are not able to extract it outside of the Fake instance.
  3. The when functions are responsible for the “teaching” of the fake.
    — For the first parameter it is possible to give the exact function name as a String or with a keyPath. KeyPath needs some extra work to store the function name Strings as properties in the fakes, but it will make any future modifications easier, e.g. whenever a method signature is changed, the names only have to be modified in one place instead of every test case they were set up.
    — The second parameter is the FakedFunction closure where we can add the logic we expect from the faked method, being able to monitor the called parameters and the invocation count.
  4. handleCall functions are the ones that find the previously registered FakedFunction after that makes increase the `invocationCount` for it, convert the parameters into a wrapper, to be able to easily cast it internally to avoid the unnecessary type casts in the closure, invoke the FakedFunction closure with the given parameters, make the necessary typecast for the result of the closure and return with it.
    — The first parameter here is identifying the function itself with the function name or with a specific keypath, just like in the case of the `when` function
    — The second parameter is a `vararg` to pass in the parameters of the function. We use `varargs` because this is the most convenient way for passing the parameters here.
  5. props will return all of the properties of the fake instance in a `String: Any` dictionary, making use of Mirror.
  6. assertProp helps to check if a property equals an expected value. The only restriction is that the property has to conform to the Equatable protocol. For other types of properties, we made some extensions to be able to compare with other values but this is not included here.
  7. tearDown function is responsible for removing all of the registered values for the instance. For now, our recommendation is to use it in the tearDown in every test class.

But what if there are multiple instances of fakes?

For this, we created a FakedFunctionHolder.

This struct is also in the Faked file, its access modifier is `fileprivate` so it’s only accessible from this file. The `functionDetails` static property is where we store the `invocationCount` and FakedFunction closure as a tuple — the value in the inner dictionary — the key is the function name or unique id for this and all of this is under the instanceId — outer dictionary — key. Actually, this is the basic concept behind the whole solution.

fileprivate struct FakedFunctionHolder {

private static var functionDetails = [String: [String: (Int, FakedFunction)]]()

static func add(instanceId: String, functionName: String, functionDetail: (Int, FakedFunction)) {
FakedFunctionHolder.functionDetails[instanceId] = [functionName: functionDetail]
}

static func get(instanceId: String, functionName: String) -> (Int, FakedFunction)? {
let functionDetails = FakedFunctionHolder.functionDetails[instanceId]
return functionDetails?[functionName]
}

static func remove(instanceId: String) {
FakedFunctionHolder.functionDetails[instanceId] = nil
}
}

Usage

Let’s imagine we have a class that makes network calls through a network client which is an injected dependency.

Here is a class just for a simple example. It doesn’t do anything fancy, just calls a client to get a personalized greeting with the name added as a parameter in the greet function.

import Foundation

class GreetingService {
let networkClient: NetworkClient

init(networkClient: NetworkClient) {
self.networkClient = networkClient
}

func greet(name: String) async -> [String: String] {
let url = URL(string: "https://www.example-greeting-message.com")!
let requestBody = [
"name": name
]

let request = URLRequest.create(url: url, method: .GET, body: requestBody.toData())
let result: ([String: String], HTTPURLResponse) = try! await networkClient.send(request: request)

return result.0
}
}

This is the protocol that defines what are the required implementations in our network client.

import Foundation

protocol NetworkClient {

func send<Output: Decodable>(request: URLRequest) async throws -> (Output, HTTPURLResponse)
func send<Input: Encodable, Output: Decodable>(request: URLRequest, body: Input) async throws -> (Output, HTTPURLResponse)

}

So if we want to create a Faked solution for our protocol, we only have to create a class or struct, which conforms to the NetworkClient and the Faked protocols. If we want to use keyPath for function identification, create a simple String property, add a unique id for that on instance level and when using the `handleCall` function, just pass it as a parameter.

import Foundation

struct FakeGenericNetworkClient: NetworkClient, Faked {

let instanceId = UUID().description

let send: String = "send"
let sendWithBody: String = "sendWithBody"

func send<Output>(request: URLRequest) async throws -> (Output, HTTPURLResponse) where Output: Decodable {
return try handleCall(\.send, params: request)
}

func send<Input, Output>(request: URLRequest, body: Input) async throws -> (Output, HTTPURLResponse) where Input: Encodable, Output : Decodable {
return try handleCall(\.sendWithBody, params: request, body)
}
}

And here is how to use our freshly created FakeGenericNetworkClient. We are able to fake how the `networkClient` will work as our dependency, check how many times the send method was invoked, declare what it should return, etc.

import XCTest

final class GreetingServiceTests: XCTestCase {

var fakeNetworkClient: FakeGenericNetworkClient!
var greetingService: GreetingService!

override func setUpWithError() throws {
fakeNetworkClient = FakeGenericNetworkClient()
greetingService = GreetingService(networkClient: fakeNetworkClient)
}

override func tearDownWithError() throws {
fakeNetworkClient.tearDown()
}

func testGreet_shouldGetGreetingWithName() async throws {
let name = "Reader"
let expectedBody = ["name": name]
let expectedResult = [
"greeting": "Hello dear Reader!"
]

fakeNetworkClient.when(\.send) { invocationCount, params in
let request: URLRequest! = try params[0].unwrap()
guard let body = request.httpBody?.toDict() as? [String: String] else {
throw Errors.mappingFailed("Mapping failed")
}

XCTAssertTrue(expectedBody.equals(dict: body))
XCTAssertEqual(invocationCount, 1)

return (["greeting": "Hello dear Reader!"], HTTPURLResponse())
}

let result = await greetingService.greet(name: name)

XCTAssertTrue(result.equals(dict: expectedResult))
}
}

Conclusion

This was a very easy example and the limitations of this solution were not yet fully discovered, but we are using it on a daily basis and trying to come up with new ideas to perfect it. It will never be as convenient as mocking, but we have to live with this limitation of the language itself while maintaining full control of how we are doing testing in our codebase.

Of course, this solution doesn’t work for everybody, but it solves our testing issues so far. You can find the source code on this link.

--

--