Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

How to mock Alamofire and URLSession requests in Swift

Mocking data requests that are triggered by either Alamofire or URLSession is something we all run into when we start writing tests for our networking layer. When writing tests, it’s important that we don’t actually run the requests so we don’t mess up our backend database with dummy data. Also, this allows us to run our tests offline if needed and it takes away flakiness of tests.

Although many blog posts suggest using dependency injection for mocking tests, it was not what we ended up doing at WeTransfer. It’s definitely a great technique for writing tests in general but it also requires you to alternate code implementations for the sake of testing. Therefore, we decided to write a code solution that allows us to mock data requests using a URLProtocol and prevents us from touching the actual code implementation.

What is a URLProtocol and why should I use it for mocking?

In my blog post Printing data requests using a custom URLProtocol I’ve already explained in more detail how you can use a URLProtocol. You can basically add a custom URLProtocol to your URLSession or Alamofire manager which will then be called for every outgoing request.

Apple describes the class as follows:

An abstract class that handles the loading of protocol-specific URL data.

The class allows us to handle the loading but also manipulate the loading of URL data. In other words, it allows us to alternate the response before it’s been called.

Introducing Mocker: a framework to simplify the mocking of requests

I could dive into writing a custom URLProtocol solution to make it possible to mock requests in this blog post. However, that would require quite some extra explanation of implementation details while we’ve already sorted that out for you in our Mocker framework.

At WeTransfer we’re using Mocker in all our private and public projects like, for example, GitBuddy. We’ve been developing it since 2017 and added a lot of features over the years to suit our needs. This makes it a robust framework with support for many cases including edge cases like redirects.

How does it work?

Before we dive into implementation details it’s good to understand how the Mocker framework works. In its core, it’s making use of the MockingURLProtocol which is responsible for catching requests and returning registered mocked data.

Mocks are registered inside a shared instance of the Mocker struct by instantiating a new Mock instance. All the interesting setup logic can be found in this Mock struct which includes all specific logic to match your request.

After registering the MockingURLProtocol it’s no longer possible for a request to hit the server. Whenever a request is executed without a matching Mock, a print statement will be shown:

? No mocked data found for url Optional("https://api.example.com/items") method Optional("GET"). Did you forget to use `register()`? ? 

This improves debugging while at the same time preventing you from forgetting to mock a certain test.

How do you stay current as a Swift developer?

Let me do the hard work and join 19,144 developers that stay up to date using my weekly newsletter:

Mocking Alamofire data requests

Mocking Alamofire requests can be done by registering the MockingURLProtocol in your manager. This is the only part in which we have to change our code implementation as we have to inject the URLProtocol for tests.

let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])

let sessionManager = Session(configuration: configuration)

After that, we can run our test to verify that our mocking URLProtocol is correctly registered. For this example, imagine us having an API to get a user by using the https://api.example.com/user endpoint. It results in a very simply JSON response of a user with a single name property:

struct User: Codable {
    let name: String
}

We could write a test for this endpoint as follows:

/// It should correctly fetch and parse the user.
func testUserFetching() {
    let configuration = URLSessionConfiguration.af.default
    configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])
    let sessionManager = Session(configuration: configuration)

    let apiEndpoint = URL(string: "https://api.example.com/user")!
    let expectedUser = User(name: "Antoine van der Lee")
    let requestExpectation = expectation(description: "Request should finish")

    sessionManager
        .request(apiEndpoint)
        .responseDecodable(of: User.self) { (response) in
            XCTAssertNil(response.error)
            XCTAssertEqual(response.value, expectedUser)
            requestExpectation.fulfill()
        }.resume()

    wait(for: [requestExpectation], timeout: 10.0)
}

In this test we’ve done the following:

  • We’ve registered the MockingURLProtocol with our Alamofire manager
  • We execute the request to fetch the user from our endpoint
  • We validate in the response that the user is matching our expected outcome user with the name “Antoine van der Lee”

After running this test we can see that our MockingURLProtocol is registered correctly as the following error is printed out in the console:

? No mocked data found for url Optional("https://api.example.com/user") method Optional("GET"). Did you forget to use `register()`? ?

It’s time to register a Mock for this request so our test succeeds.

Registering a Mock

To make our test succeed we need to create a Mock that’s returning a single user JSON response with a user that takes the name “Antoine van der Lee”. The great thing is: we can simply use our user instance that conforms to the Encodable protocol!

We do this by creating a new Mock instance that’s initiated with the endpoint URL and our JSON data:

let mockedData = try! JSONEncoder().encode(expectedUser)
let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData])
mock.register()

Let’s go over the steps taken:

  • Our expectedUser instance is converted to JSON data using the JSON encoder
  • A Mock instance is created using the JSON data and API endpoint
  • The data type is set to JSON and the expected status code is set to 200

As you can see, we are able to specify the response in detail. This allows you to write tests for image responses, 400 response codes, and more. All details on possibilities can be found in the Mocker readme.

Our test finally looks as follows:

/// It should correctly fetch and parse the user.
func testUserFetching() {
    let configuration = URLSessionConfiguration.af.default
    configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])
    let sessionManager = Session(configuration: configuration)

    let apiEndpoint = URL(string: "https://api.example.com/user")!
    let expectedUser = User(name: "Antoine van der Lee")
    let requestExpectation = expectation(description: "Request should finish")

    let mockedData = try! JSONEncoder().encode(expectedUser)
    let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData])
    mock.register()

    sessionManager
        .request(apiEndpoint)
        .responseDecodable(of: User.self) { (response) in
            XCTAssertNil(response.error)
            XCTAssertEqual(response.value, expectedUser)
            requestExpectation.fulfill()
        }.resume()

    wait(for: [requestExpectation], timeout: 10.0)
}

With our Mock, our test succeeds and the registered JSON data is returned as the response, great!

Mocking URLSession requests

Mocking URLSession requests works almost the same as with Alamofire. We first have to register the MockingURLProtocol with our URLSession instance:

let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])
let sessionManager = URLSession(configuration: configuration)

After that, we can run our request and validate the outcome. For this, we’re using the exact same Mock setup:

let mockedData = try! JSONEncoder().encode(expectedUser)
let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData])
mock.register()

The final test looks as follows:

/// It should correctly fetch and parse the user.
func testUserFetchingURLSession() {
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])
    let sessionManager = URLSession(configuration: configuration)

    let apiEndpoint = URL(string: "https://api.example.com/user")!
    let expectedUser = User(name: "Antoine van der Lee")
    let requestExpectation = expectation(description: "Request should finish")

    let mockedData = try! JSONEncoder().encode(expectedUser)
    let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData])
    mock.register()

    sessionManager.dataTask(with: apiEndpoint) { (data, response, error) in
        defer { requestExpectation.fulfill() }
        do {
            if let error = error {
                throw error
            }

            let user = try JSONDecoder().decode(User.self, from: data!)
            XCTAssertEqual(user, expectedUser)
        } catch {
            XCTFail(error.localizedDescription)
        }
    }.resume()

    wait(for: [requestExpectation], timeout: 10.0)
}

And that’s all it takes to mock your URLSession requests in a unit test.

Mocking other common use-cases

Now that we know how to write a mock for both Alamofire and URLSession we can look into a few common use-cases for writing mocks.

Verifying that a request has been called

In some cases, you just want to know whether a certain request is called. You can verify this by making use of the completion callback on a Mock:

let mockExpectation = expectation(description: "The content deletion mock should be called")
var mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 204, data: [.delete: Data()])
mock.completion = {
    mockExpectation.fulfill()
}
mock.register()

We’ve set the response code to be 204 No Content as expected by our implementation and we set the completion callback before we register the mock.

A final test implementation could look as follows:

/// It should call the delete API endpoint.
func testDeletingContent() {
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? [])
    let sessionManager = URLSession(configuration: configuration)
    /// Setup the deleter with our URLSession that's containing the URLProtocol.
    let deleter = ContentDeleter(sessionManager: sessionManager)

    // Setup the API endpoint
    let contentIdentifier = UUID().uuidString
    let apiEndpoint = URL(string: "https://api.example.com/content?id=\(contentIdentifier)")!

    // Register the mock
    let mockExpectation = expectation(description: "The content deletion mock should be called")
    var mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 204, data: [.delete: Data()])
    mock.completion = {
        mockExpectation.fulfill()
    }
    mock.register()

    let contentDeletionExpectation = expectation(description: "Completion should be called after deletion")
    deleter.deleteContent(identifier: contentIdentifier) {
        contentDeletionExpectation.fulfill()
    }
    /// Verify both expectations are met in the right order.
    wait(for: [mockExpectation, contentDeletionExpectation], timeout: 10.0, enforceOrder: true)
}

Verifying POST body arguments

Your code is often setting up a request with a certain amount of arguments. It’s good to verify that your code is using the expected arguments when executing a request. For this, you can make use of the onRequest property on a Mock that gets called with the request and its post body arguments:

let mockExpectation = expectation(description: "The content post mock should be called")
var mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.post: mockedData])

mock.onRequest = { request, httpBodyArguments in

    /// Verify the correct base URL
    XCTAssertEqual(request.url?.absoluteString, "https://api.example.com/board/\(boardIdentifier)")

    /// Verify the POST arguments
    XCTAssertEqual(httpBodyArguments as? [String: String], [
        "identifier": contentIdentifier,
        "name": expectedContentName
    ])
    mockExpectation.fulfill()
}
mock.register()

Verifying GET arguments

The same we could do for the GET arguments by ignoring the post body arguments and simply extracting the GET arguments from the URLRequest:

let mockExpectation = expectation(description: "The get content list mock should be called")
var mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData])

mock.onRequest = { request, _ in

    /// Verify the correct base URL
    let urlWithoutQuery = request.url?.absoluteString.replacingOccurrences(of: request.url!.query!, with: "")
    XCTAssertEqual(urlWithoutQuery, "https://api.example.com/board/\(boardIdentifier)?")

    /// Verify the correct Query parameters
    XCTAssertEqual(request.url?.query, "limit=20&offset=0")

    mockExpectation.fulfill()
}
mock.register()

Conclusion

That’s it! We’ve covered the basics of mocking Alamofire and URLSession requests by making use of the Mocker framework. The framework has a lot more possibilities which you can find in the readme of the project. Hopefully, this post makes it easier for you to write and maintain mocked data requests.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.