HTTP request helper, is one of the most essential parts of most modern-day apps. It takes care of all the processing works before and after calling a remote API.

Ideally the implementation of HTTP request helper should start after the remote APIs are ready. However, in most cases, due to tight project schedule, mobile developers will not be given the luxury to start their development work after the remote APIs are ready.

In this kind of situation, some developers might rework their development plan to work on UI related features first while waiting for the server to get ready. Some others might setup a temporary local server to test out their networking module.

In this article, I will show you my way of dealing with this kind of situation — using test doubles to replicate the output from the remote APIs.

If you are not familiar with the concept of test doubles in Swift, feel free to check out this article that explains in detail the concept of dummy, fake, stub and mock.

🔗 Test Doubles in Swift: Dummy, Fake, Stub, Mock

The best way to demonstrate how stubbing and mocking a remote API can be done is by using a real life example.

Without further ado, let’s get started!

Disclaimer: The following example might not be fully compliant with the RESTful API Designing guidelines and best practices. This is intended in order to reduce the complexity of the example.

The Overall Architecture

In our example, let’s implement a HTTP request helper that performs a POST request that register a new user to the server.

Following diagram shows the overall architecture of our example.

The networking module architecture

The registration request helper is the class that we are going to implement. It takes care of all the operations required before performing the registration POST request, such as password encryption and JSON encoding.

Furthermore, it is also in charge of handling responses from the network layer, such as JSON decoding and error handling.

The encryption helper is a utility class that is responsible for password encryption. Here’s the skeleton of the EncryptionHelper class.

protocol EncryptionHelperProtocol { func encrypt(_ value: String) -> String } class EncryptionHelper: EncryptionHelperProtocol { func encrypt(_ value: String) -> String { // Encryption logic here // ... // ... return "some encrypted value"; } }

Another class to take note in our example is the network layer. It contains all the URLSession and URLSessionTask related code.

Following is the simplified skeleton of the NetworkLayer class. Usually it should contain other methods such as get() , put() and delete() . However, for demonstration purposes, we will only focus on the post() methods.

protocol NetworkLayerProtocol { func post(_ url: URL, parameters: Data, completion: (Result<Data, Error>) -> Void) } class NetworkLayer: NetworkLayerProtocol { /// Perform POST request /// - Parameters: /// - url: Remote API endpoint /// - parameters: Request's JSON data /// - completion: Completion handler that either return response's JSON data or error func post(_ url: URL, parameters: Data, completion: (Result<Data, Error>) -> Void) { // Create URL session // Create session task // ... // Perform POST request // ... // ... // Trigger completion handler } }

Note that the actual implementation for both NetworkLayer and EncryptionHelper are not important, what’s important is their protocol. This is because we are going to create test doubles based on their protocol in a short while.

The Prerequisites

Before we dive into the RegistrationRequestHelper class’s implementation, there are a few things we need to get it out of our way.

Identify the RegistrationRequestHelper ‘s dependencies. Finalise the request JSON’s structure. Finalise the response JSON’s structure. Define all possible RegistrationRequestHelper ‘s errors. Define unit test cases for RegistrationRequestHelper .

Identify the RegistrationRequestHelper’s Dependencies

As mentioned earlier in this article, we will be creating test doubles to test out the RegistrationRequestHelper , and all the RegistrationRequestHelper ‘s dependencies will be the test doubles that we need to create.

By using the architecture diagram, we can easily identify the RegistrationRequestHelper ‘s dependencies — NetworkLayer and EncryptionHelper .

Finalise the request JSON’s structure

In order to implement the RegistrationRequestHelper , we need to know the request JSON’s structure. In real life, the server side developer should provide this information.

Furthermore, the request JSON will most likely contain a lot of information. However, for simplicity sake, let’s assume the request JSON’s structure is as follows.

{ "username": "swift-senpai", "password": "abcd1234" }

Finalise the response JSON’s structure

Next up is to finalise the response JSON’s structure when the API call successful. We need this information so that we can implement the JSON parser correctly.

Let’s assume the server will respond with a user object as shown below.

{ "user_id": 12345, "username": "swift-senpai", "email": null, "phone": null }

Based on the above sample JSON, we can create a User class that conform to the Decodable protocol, so that we can use the JsonDecoder class for parsing later.

struct User: Decodable { let userId: Int let username: String let email: String? = nil let phone: String? = nil }

Define all possible RegistrationRequestHelper’s errors

Do not forget that there might be situations where error occurred during the API call. Thus our RegistrationRequestHelper will have to handle all the possible errors that might occur.

Again, for simplicity sake, let’s assume there are only 3 possible errors:

Username already exists — User provided a username that already exist Unexpected response — Failed to parse the response JSON POST request failed — All other errors

The “username already exists” error should trigger when we receive an error JSON from server. Let’s just assume the JSON is as shown below.

{ "error_code": "E001", "message": "Username already exists" }

Following is the RegistrationRequestError enum.

enum RegistrationRequestError: Error, Equatable { case usernameAlreadyExists case unexpectedResponse case requestFailed }

Do note that we conformed the RegistrationRequestError enum to the Equatable protocol, this is especially useful when we want to verify the RegistrationRequestHelper ‘s output during unit test.

Define unit test cases for RegistrationRequestHelper

Lastly, let’s define the unit test cases that we need to ensure that the RegistrationRequestHelper is working correctly. Following are the verifications that are required.

It is posting to the correct URL.

Password is encrypted before posting.

The request JSON’s structure is correct.

The response JSON is parsed correctly.

The usernameAlreadyExists error is handled correctly.

error is handled correctly. The unexpectedResponse error is handled correctly.

error is handled correctly. The requestFailed error is handled correctly.

With all the prerequisites out of the way, is time to buckle up and dive into the RegistrationRequestHelper class’s implementation.

The Implementation

Let’s start by implementing the RegistrationRequestHelper class’s initialiser. We will use an initialiser-based dependency injection to inject both networkLayer and encryptionHelper into the helper class.

protocol RegistrationHelperProtocol { func register(_ username: String, password: String, completion: (Result<User, RegistrationRequestError>) -> Void) } class RegistrationRequestHelper: RegistrationHelperProtocol { private let networkLayer: NetworkLayerProtocol private let encryptionHelper: EncryptionHelperProtocol // Inject networkLayer and encryptionHelper during initialisation init(_ networkLayer: NetworkLayerProtocol, encryptionHelper: EncryptionHelperProtocol) { self.networkLayer = networkLayer self.encryptionHelper = encryptionHelper } }

Next we will add a register() method that accepts a username, password and completion handler.

The register() method will encrypt the given password, encode all post parameters to JSON data and perform the POST request using the given networkLayer .

func register(_ username: String, password: String, completion: (Result<User, RegistrationRequestError>) -> Void) { // Remote API URL let url = URL(string: "https://api-call")! // Encrypt password using encryptionHelper let encryptedPassword = encryptionHelper.encrypt(password) // Encode post parameters to JSON data let parameters = ["username": username, "password": encryptedPassword] let encoder = JSONEncoder() let requestData = try! encoder.encode(parameters) // Perform POST request using network layer networkLayer.post(url, parameters: requestData) { (result) in // Completion handler logic here... } }

The final part that we need to implement is the networkLayer ‘s completion handler. We will have to handle both the success and failure case of the returned Result type.

For the success case, we need to handle 2 types of JSON, the user object JSON and the error JSON. To recap, here are the sample JSONs.

User object JSON and Error JSON

Since we already created a User class that conform to the Decodable protocol, we can easily parse the user object JSON by using the JsonDecoder class.

For the error JSON, we can use JSONSerialization class to parse and grab the error_code ‘s value, so that we can identify what type of errors are occurring.

If we fail to parse the response JSON from the server, we will return the unexpectedResponse error.

For the failure case, we will just return the requestFailed error.

The above explanation might be a little bit overwhelming, however the sample code below should be able to clear things up for you.

networkLayer.post(url, parameters: requestData) { (result) in switch result { case .success(let jsonData): // Create JSON decoder to decode response JSON to User object let decoder = JSONDecoder() // Convert JSON key from snake case to camel case // Ex: user_id --> userId decoder.keyDecodingStrategy = .convertFromSnakeCase if let user = try? decoder.decode(User.self, from: jsonData) { // Parsing JSON to user object successful // Trigger completion handler with user object completion(.success(user)) return } else if let error = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] { // Parsing JSON to get error code if let errorCode = error["error_code"] as? String { // Error code available // Use error code to identify the error switch errorCode { case "E001": completion(.failure(.usernameAlreadyExists)) return default: break } } } // Failed to parse response JSON // Trigger completion handler with error completion(.failure(.unexpectedResponse)) case .failure: // HTTP Request failed // Trigger completion handler with error completion(.failure(.requestFailed)) } }

With that, we have done implementing the RegistrationRequestHelper . Here’s the full implementation of it.

class RegistrationRequestHelper: RegistrationHelperProtocol { private let networkLayer: NetworkLayerProtocol private let encryptionHelper: EncryptionHelperProtocol // Inject networkLayer and encryptionHelper during initialisation init(_ networkLayer: NetworkLayerProtocol, encryptionHelper: EncryptionHelperProtocol) { self.networkLayer = networkLayer self.encryptionHelper = encryptionHelper } func register(_ username: String, password: String, completion: (Result<User, RegistrationRequestError>) -> Void) { // Remote API URL let url = URL(string: "https://api-call")! // Encrypt password using encryptionHelper let encryptedPassword = encryptionHelper.encrypt(password) // Encode post parameters to JSON data let parameters = ["username": username, "password": encryptedPassword] let encoder = JSONEncoder() let requestData = try! encoder.encode(parameters) // Perform POST request using network layer networkLayer.post(url, parameters: requestData) { (result) in switch result { case .success(let jsonData): // Create JSON decoder to decode response JSON to User object let decoder = JSONDecoder() // Convert JSON key from snake case to camel case // Ex: user_id --> userId decoder.keyDecodingStrategy = .convertFromSnakeCase if let user = try? decoder.decode(User.self, from: jsonData) { // Parsing JSON to user object successful // Trigger completion handler with user object completion(.success(user)) return } else if let error = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] { // Parsing JSON to get error code if let errorCode = error["error_code"] as? String { // Error code available // Use error code to identify the error switch errorCode { case "E001": completion(.failure(.usernameAlreadyExists)) return default: break } } } // Failed to parse response JSON // Trigger completion handler with error completion(.failure(.unexpectedResponse)) case .failure: // HTTP Request failed // Trigger completion handler with error completion(.failure(.requestFailed)) } } } }

Phew! We have come a long way in implementing the RegistrationRequestHelper . Now is time to test it out.

The Testing

We have finally reached the most important part of this article. We will use the test doubles concept in unit test to replicate the behaviour of an actual working server.

Here’s a quick recap on what we wanted to verify using unit test.

It is posting to the correct URL.

Password is encrypted before posting.

The request JSON’s structure is correct.

The response JSON is parsed correctly.

The usernameAlreadyExists error is handled correctly.

error is handled correctly. The unexpectedResponse error is handled correctly.

error is handled correctly. The requestFailed error is handled correctly.

Let’s start with the first 3 items. We will group them into 1 XCTest test case because all of these 3 items can be verified by using the same test doubles. Furthermore, these 3 items are all related to configurations before making a POST request.

Verifications Before POST Request

In the prerequisites section, we have already identified the dependencies for the RegistrationRequestHelper class — networkLayer and encryptionHelper . We will start by creating test doubles of these 2 classes.

class MockNetworkLayer: NetworkLayerProtocol { var postUrl = URL(string: "http://dummy.com")! var requestData = Data() func post(_ url: URL, parameters: Data, completion: (Result<Data, Error>) -> Void) { // Keep track on the given url and parameters value postUrl = url requestData = parameters // Trigger completion with dummy data completion(.success(Data())) } } class MockEncryptionHelper: EncryptionHelperProtocol { var encryptCalled = false func encrypt(_ value: String) -> String { // Set flag to true When encrypt(_:) is called encryptCalled = true // Return a dummy value (this value is not important) return "1234567890" } }

From the code snippet above, you can see that we are declaring variables within the test doubles to keep track on the parameters that we wanted to verify. We call this kind of unit test strategy “mocking”.

With the test doubles ready, we can now start writing our unit test. Note that we will be using the AAA pattern (Arrange-Act-Assert) to write the unit test.

/// Assert configurations and parameters for the post request are correct func testBeforePostRequest() { /* Arrange */ // Create dependencies let username = "swift-senpai" let password = "abcd1234" let mockNetworkLayer = MockNetworkLayer() let mockEncryptionHelper = MockEncryptionHelper() // Set expectation let exp = expectation(description: "Post request completed") /* Act */ // Perform post request let requestHelper = RegistrationRequestHelper(mockNetworkLayer, encryptionHelper: mockEncryptionHelper) requestHelper.register(username, password: password) { (result) in exp.fulfill() } waitForExpectations(timeout: 5.0, handler: nil) /* Assert */ // Assert post url is correct let expectedUrl = URL(string: "https://api-call")! let actualUrl = mockNetworkLayer.postUrl XCTAssertEqual(actualUrl, expectedUrl) // Assert encrypt is called XCTAssertTrue(mockEncryptionHelper.encryptCalled) // Assert post parameters are correct let encryptedPassword = mockEncryptionHelper.encrypt(password) var expectedRequestJson = """ { "username": "\(username)", "password": "\(encryptedPassword)" } """ expectedRequestJson.trimJSON() let actualRequestData = mockNetworkLayer.requestData let actualRequestJson = String(data: actualRequestData, encoding: .utf8)! XCTAssertEqual(expectedRequestJson, actualRequestJson) } // MARK:- Utilities extension String { mutating func trimJSON() { self = self.replacingOccurrences(of: "

", with: "") self = self.replacingOccurrences(of: " ", with: "") } }

There are 2 things to take note here.

First, note that we are verifying that the mockEncryptionHelper ‘s encrypt(_:) method has been called. We are only interested in knowing whether the password is being encrypted, the correctness of the encryption logic is not important here.

In order to verify the correctness of the encryption logic, we should create another dedicated test plan for that, however that is beyond the scope of this article.

Second, note that in order to verify that the post parameters are correct, we are using JSON string for assertion instead of JSON data. This is because JSON string is more visualisable compared to JSON data.

To build and run the test, press ⌘Q. You should see a “Test Succeeded” notification from Xcode if you have followed along everything correctly.

Verifications After POST Request

Next up, let’s verify that the response JSON is parsed to User object correctly.

For this test case, the test doubles that we need is a dummy encryptionHelper and a networkLayer that return a user object JSON data.

Note that in the code snippet below, the technique that we used to force a specific output from the networkLayer ‘s post() method is called “Stubbing”.

/// Dummy object that do nothing class DummyEncryptionHelper: EncryptionHelperProtocol { func encrypt(_ value: String) -> String { return value } } /// NetworkLayer stub that return success result class StubSuccessNetworkLayer: NetworkLayerProtocol { func post(_ url: URL, parameters: Data, completion: (Result<Data, Error>) -> Void) { let responseJson = """ { "user_id": 1001, "username": "swift-senpai", "email": null, "phone": null } """ // Convert JSON string to JSON data let jsonData = Data(responseJson.utf8) completion(.success(jsonData)) } }

The way to utilise the above test doubles is fairly straightforward.

/// Assert that the parsing of User object from JSON is working correctly func testParseUserObject() { /* Arrange */ // Create dependencies let stubNetworkLayer = StubSuccessNetworkLayer() let dummyEncryptionHelper = DummyEncryptionHelper() // Set expectation let exp = expectation(description: "Post request completed") /* Act */ // Perform post request var postResult: Result<User, RegistrationRequestError>! let requestHelper = RegistrationRequestHelper(stubNetworkLayer, encryptionHelper: dummyEncryptionHelper) requestHelper.register("dummy-username", password: "dummy-password") { (result) in // Capture post result postResult = result exp.fulfill() } waitForExpectations(timeout: 5.0, handler: nil) /* Assert */ let actualUser = try? postResult.get() let expectedUser = User(userId: 1001, username: "swift-senpai") // Assert user ID is correct XCTAssertEqual(expectedUser.userId, actualUser?.userId) // Assert username is correct XCTAssertEqual(expectedUser.username, actualUser?.username) // Assert email & phone is nil XCTAssertNil(actualUser?.email) XCTAssertNil(actualUser?.phone) }

By using the same stubbing strategy, we can proceed to the next test case — Verify that the usernameAlreadyExists error is being handled correctly.

Below is the required test double.

/// NetworkLayer stub that return "username already exists" error class StubUsernameExistNetworkLayer: NetworkLayerProtocol { func post(_ url: URL, parameters: Data, completion: (Result<Data, Error>) -> Void) { let responseJson = """ { "error_code" : "E001", "message":"Username already exists" } """ // Convert JSON string to JSON data let jsonData = Data(responseJson.utf8) completion(.success(jsonData)) } }

Here’s the test case.

/// Assert that the username already exists error will trigger func testUsernameAlreadyExists() { /* Arrange */ // Create dependencies let stubNetworkLayer = StubUsernameExistNetworkLayer() let dummyEncryptionHelper = DummyEncryptionHelper() // Set expectation let exp = expectation(description: "Post request completed") /* Act */ // Perform post request var postResult: Result<User, RegistrationRequestError>! let requestHelper = RegistrationRequestHelper(stubNetworkLayer, encryptionHelper: dummyEncryptionHelper) requestHelper.register("dummy-username", password: "dummy-password") { (result) in // Capture post result postResult = result exp.fulfill() } waitForExpectations(timeout: 5.0, handler: nil) /* Assert */ var actualError: RegistrationRequestError? XCTAssertThrowsError(try postResult.get()) { (error) in actualError = error as? RegistrationRequestError } let expectedError = RegistrationRequestError.usernameAlreadyExists XCTAssertEqual(expectedError, actualError) }

As you can see, the test doubles and test cases above are very similar. Thus, I will leave the final 2 test cases for you.

The unexpectedResponse error is handled correctly.

error is handled correctly. The requestFailed error is handled correctly.

By using the stubbing technique that we discussed just now, you should be able to get them done without any problem.

In case you get stuck in the last 2 test cases, you can find the full sample code and unit test cases here.

Wrapping Up

That’s it! This is how I developed and tested the entire RegistrationRequestHelper class without depending on a real life working server.

By using the mocking and stubbing strategy in test doubles, we manage to replicate the output of the remote APIs. In fact, I would recommend every developer to perform unit tests on their networking module even though a working server is available.

Here are some other benefits you can get by unit testing your networking module:

The test cases can act as executable documentation. You feel more confident as you know your code is working properly. Unit test cases can be executed anytime even without an internet connection. Unit test cases are easy and fast to execute.

Next time when you need to implement a HTTP request helper without a working server, just remember this…

Further Readings

I hope this article gives you a good inspiration on how you can use unit tests to aid your daily development work.

If you like this article, feel free to share it. Let me know your thoughts in the comment section below.

Follow me on Twitter for more articles related to iOS development.

Thanks for reading and happy coding. 👨🏼‍💻