Learning to Mock in Swift
Stubbing out nested functions in unit tests

Depending on where you stand on unit testing, you may find this article more or less useful than others. I choose to test because when I develop for myself I don’t have someone who can offer a code review, so it gives me that extra level of reassurance that I’m not breaking anything. I also find it oddly satisfying seeing everything go green.
Why the Need to Mock?
“Unit testing is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software.”
— International Software Testing Qualifications Board (ISTQB)
To unit test a function well, you need to cover all the different routes your function can take. As your application grows, your functions will end up calling nested functions you’ve written in your codebase or other library functions.
When you come to unit testing a function, you may be in a position where you’re thinking about what the output of those nested functions and want to test a certain path.
This is where mocking comes in. It enables you to solely test the function in question without having to think about the logic in the nested functions. For example, if your function is going to make an API call, you can mock the networking layer so your tests don’t actually make the call but still account for all possible scenarios.
As you can imagine, this speeds up your tests dramatically and means you’re not relying on the network for your tests to pass or fail.
Mocking in Swift
By looking at a typical network layer, we can see what the issue is and what can be done with Swift’s protocols to mock out functions.
public final class NetworkClient {
private let session: URLSession = .shared
public init() { }
func executeRequest(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
session.dataTask(with: request) { (data, _, error) in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
completion(.success(data))
}.resume()
}
}As you can see, if we were to test the executeRequest function, there’s no way of doing it without invoking a real URLSession and making a request on the URL provided in the arguments. Instead, we can refactor the class to make use of protocols, which will allow us to inject a mock in our tests.
protocol NetworkClientProtocol {
func executeRequest(request: URLRequest, completion: @escaping (Data?, Error?) -> Void)
}
extension URLSession: NetworkClientProtocol{
/// this is where the real request happens
func executeRequest(request: URLRequest, completion: @escaping (Data?, Error?) -> Void) {
let task = dataTask(with: request) { (data, _, error) in
completion(data, error)
}
task.resume()
}
}We create a protocol with the function’s signature. We then make the real implementation of the function by making URLSession conform to the NetworkClientProtocol. This is easier to understand once we look at how we can invoke it.
public final class NetworkClient {
private let n: NetworkClientProtocol
init(n: NetworkClientProtocol = URLSession.shared) {
self.n = n
}
func executeRequest(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
n.executeRequest(request: request){ (data, error) in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
completion(.success(data))
}
}
}By refactoring the NetworkClient, we can pass in anything that conforms to the protocol we created at the initialisation stage and default to a normal URLSession.shared, like we did before.
Then when we call executeRequest, it uses that value we passed in to call its own implementation of the function. Thereby, we only invoke the network request when we’re using a real URLSession. Lets now see how we can mock it in our tests.
func testExecuteRequest_failure_noData() throws {
/// create mock to return nil
class NetworkMock : NetworkClientProtocol{
func executeRequest(request: URLRequest, completion: @escaping (Data?, Error?) -> Void) {
completion(nil, nil)
}
}
/// inject mock and execute request
var networkResultOptional : Result<Data, Error>?
let client = NetworkClient(p: NetworkMock())
client.executeRequest(request: URLRequest.station(station: Station(id: "BHM"))){ result in
networkResultOptional = result
}
/// assert
guard let networkResult = networkResultOptional else {
return XCTFail("NetworkClient Result is nil")
}
switch networkResult{
case .success(let data):
return XCTFail("NetworkClient was meant to fail with a noData error but returned success with data \(data)")
case .failure(let error):
XCTAssertEqual(error.localizedDescription, NetworkError.noData.localizedDescription)
}
}We mock the function by creating a class on the fly in our tests that conforms to the NetworkClientProtocol. Here we can then tell it to do whatever we want. In this case, I’ve told it to return nil data to simulate the noData error I have in my function.
We then initialise the mock class we just created, pass it into the NetworkClient, and make the request and any assertions we want to make.
Using this method, we’ve avoided the real-network request, meaning our tests are much more stable, and we can test all the possible scenarios networking can bring.
Something else we get for free is not needing to use XCTestExpectation in our tests. This is usual when dealing with @escaping closures, but due to our mock function returning straight away, we can avoid it.
Conclusion
I hope you can see that this method is easily replicable with any function you’ve written in Swift. Thanks for reading!
