Network communication is a crucial part in every iOS apps.
Whenever the users interact with a UI element in the app, there usually is one (or multiple) network request sent to an API server to get fresh data and present back to the users.
Let’s look at an example network usage of a typical News Reader app:
- Request to authenticate users.
- Request to get list of news sources: Engadget, Mashable, Medium,…
- Request to get list of latest articles when users click on a news source.
- Request for more details when users click on an article.
- Request to mark all articles as read when users click on the corresponding button.
Network requests are everywhere. We should make sure that not only do we understand networking clearly but also can implement it very well.
Today we’re gonna learn how to build a simple network layer to fetch data from a RESTful API server. The process includes designing, coding and unit testing altogether.
Here’s the table of contents:
- 1. Design the protocol
- 2. Implement the protocol
- 3. Use the protocol
- 4. Write unit test for the protocol
- What to test?
- How to test?
- Prepare to test
- Start writing test
What RESTful API should we work on?
Let’s just pick an easy one.
Implement GitHub API to get a user’s information
If you click on the link https://api.github.com/users/hoang-tran in your browser. It will return a response in json format:
Simple and sweet, right? Let’s head over to the implementation in our iOS app.
1. Design the protocol:
Create a new file called GitHubApiClient.swift in your main target.
Add a protocol named GitHubApiClient:
As you can see, the API requires us to pass in username as an argument. We will reflect that in our protocol:
Now we need a way to handle callbacks when the request finishes. If it succeeds, we want to receive the user data in a nice format.
The GitHubUserData is just a struct we created to store the data obtained from the json response.
When the request fails, we want to get the error.
Let’s alias the 2 callbacks to something more readable:
Now, our protocol will look like this:
There’s time when we don’t want to specify any callbacks at all. Let’s change both of them to optionals:
Now we are done with the protocol design. Let’s move on to the coding part.
2. Implement the protocol:
There are many ways to implement network requests in iOS. The 3 most popular ones are: NSURLSession, AFNetworking and Alamofire.
This is supported natively in iOS 7.0 and above.
Remember to add
pod 'AFNetworking' to your Podfile.
Remember to add
pod 'Alamofire' to your Podfile.
3. Use the protocol:
This is fairly straightforward. Just put in:
- parameter for username.
- closure for onSuccess callback.
- closure for onError callback.
Or we can remove the callback we don’t care about.
Or remove everything.
4. Write unit test for the protocol:
What to test?
When talking about network request, there’s a couple of things we need to consider:
- Does the request go to the correct url?
- When the request succeeds, does it parse json properly and return the correct model?
- When the request fails, does it return the corresponding error?
How to test?
We will use a HTTP stubbing framework called Mockingjay.
It allows you to say this:
What does Stub mean anyway?
This is the syntax of a stub:
What it means:
When event A happens, don’t do it the old way. Do custom action B instead.
So the line:
Will translate to as:
Whenever there’s a request that goes to url A, do not send it over to network.
Instead, use the file B in our project’s local directory as the response.
This reads as:
If there’s a request that goes to url https://api.github.com/users/hoang-tran, do not send it over to network.
Instead, use the file GetUserSuccess.json as the response
So to write unit test for a network request, we will need to do things in the following order:
- Stub the request and return with our custom json file.
- Actually make the network request using the protocol.
- Describe our expectations for the returned values in both cases: onSuccess and onError.
Prepare to test:
Before we start, make sure you’re familiar with:
- How to write unit tests in iOS Part 1: XCTestCase.
- How to write unit tests in iOS Part 2: Behavior-driven Development (BDD).
- Write better unit test assertions with Nimble.
Start writing test:
Step 1: Setup project for unit testing
Open Podfile and add the following pods to your test target:
- Quick: a behavior-driven development (BDD) framework for Swift.
- Nimble: write test assertion that reads just like English.
- Mockingjay: stub network request in Swift.
Type this command into your Terminal to install all 3 pods.
Open the generated .xcworkspace file.
Step 2: Create a new spec file
Create a new file called NativeApiClientSpec.swift in your test target.
Press Cmd + U in Xcode and make sure the test pass.
Step 3: Write a test that makes real network request
We will write the success case first:
Make a real network request in the it closure:
We need to store the userData when the request succeeds:
Then we expect the returnedUserData to have value:
The toEventuallyNot means that it will continuously check the returnedUserData to see if it has value (not nil).
After a couple of seconds, if the returnedUserData has value , the test passes, otherwise it fails.
However, when you run the test (Cmd + U), it will fail.
Because currently we’re making real network request and it might take time to finish. The toEventually does not wait long enough for the request to set value for the returnedUserData and thus causes the failure.
The default wait time is 1 second. If we change it to a larger number, the test shall pass.
But we don’t wanna do that. Our goal is to NOT make real network request at all.
Step 4: Create stubbed response file
Open Terminal and go to the TestNetworkLayerTests directory (assuming your project name is TestNetworkLayer)
Create a new directory called Fixtures and then go to it:
Note that Fixtures is just a common name to refer to those stubbed response files. You can name the directory to anything.
Create a new json file called GetUserSuccess.json, then open it for edit:
Open this link https://api.github.com/users/hoang-tran in your browser.
Paste the json content to the GetUserSuccess.json file:
Open the current directory in Finder:
Drag the Fixtures directory into your test target:
Xcode will ask you for confirmation. Make sure you added the directory to the test target instead of your main target.
Step 5: Stub the network request
Back to NativeApiClientSpec.swift, we’re gonna stub the request so that it will use our GetUserSuccess.json file as the response instead of making real network request.
We did a couple of steps here:
- Get the path of the GetUserSuccess.json file.
- Read its content into a NSData object.
- Stub the url so that everytime a request goes to https://api.github.com/users/hoang-tran, it will return the fake json data without having to reach for network.
With everything stubbed, hit Cmd + U again.
This time, it should pass.
To make sure that it does not make any real network request, turn off your internet connection and run your tests again. It should still pass.
Step 6: Expect the protocol to parse json correctly
We should also write some expectations for the returnedUserData to make sure that the json response is parsed properly into the GitHubUserData struct.
Step 7: Test the error case
First declare the error context.
Stub the request to return your custom error:
The whole test case:
Run the test again and make sure it’s green.
Wow! I didn’t expect this to turn into such a looooong post. Let’s end it here.
You can find the full code sample at https://github.com/hoang-tran/TestNetworkLayer.
I’m very happy to see your comments. Please let me know:
- How do you design your network layer?
- What networking framework do you use? And why?
- Have you ever written unit tests for your network layer? If yes, how do you do that?