9.
Using the Network Client
Written by Joshua Greene
In the last chapter, you identified that ListingsViewController isn’t actually doing any networking. Rather, it has a // TODO comment in refreshData().
In response, you created DogPatchClient to handle networking logic. However, you haven’t used it yet.
In this chapter, your job is to update ListingsViewController to use DogPatchClient to actually network! Specifically, you’ll:
- Add a
sharedinstance toDogPatchClient. - Add a property for the network client on
ListingsViewController. - Create a network client protocol.
- Create a mock network client using the protocol.
- Use the mock to stub and validate behavior.
Getting started
Feel free to use your project from the last chapter. If you want a fresh start, navigate to this chapter’s starter directory, open the DogPatch subdirectory and then open DogPatch.xcodeproj.
Once your project is ready, it’s time to jump in and set DogPatchClient up for networking by adding a shared instance.
Creating a shared instance
While you could instantiate DogPatchClient directly, this has disadvantages:
- You’d have to duplicate creation data, including the
baseURL,sessionandresponseQueue, anywhere you instantiateDogPatchClient. - You’d make more network calls in parallel, since there’s a limit on the number of these you can run per
URLSession. You could harm battery or network performance as a result.
A better alternative is to add a static shared property on DogPatchClient. This uses the singleton plus pattern: You’ll use the shared instance most of the time, but you can also create one-off DogPatchClient instances, such as in your unit tests.
Before you’re allowed to write app code, you first need to write a failing test. Open DogPatchClientTests.swift and add this test right before test_init_sets_baseURL(), ignoring the compiler error like usual:
func test_shared_setsBaseURL() {
// given
let baseURL = URL(
string: "https://dogpatchserver.herokuapp.com/api/v1/")!
// then
XCTAssertEqual(DogPatchClient.shared.baseURL, baseURL)
}
You first create an expected baseURL, and you assert this equals DogPatchClient.shared.baseURL. Since you haven’t defined shared on DogPatchClient, however, this doesn’t compile.
A compiler error counts as a failing test, so you’re allowed to write app code to fix it.
Open DogPatchClient.swift, and add the following right before init(baseURL:session:responseQueue:):
static let shared = DogPatchClient(
baseURL: URL(string:"https://example.com")!,
session: URLSession(),
responseQueue: nil)
Here you’ve defined a static shared property with dummy values for its inputs. This is enough to fix the compiler error in the unit tests.
Build and run the unit tests and, as expected, this last test fails. That’s because the baseURL on DogPatchClient.shared is not equal to the expected baseURL. To make it pass, open DogPatchClient.swift and replace the baseURL input value on shared with the following:
baseURL: URL(
string:"https://dogpatchserver.herokuapp.com/api/v1/")!
Warning: Due to the way
URL(string:relativeTo:)resolves URLs, you must include the trailing slash at the end of the URL string. If you don’t, theURLcreated withingetDogswon’t include thev1in its path and, consequently, the server will not recognize it.
Build and run the unit tests; they should all pass now. However, you still need a couple more tests to ensure you’ve set the correct values for DogPatchClient.shared.
Add the following test below test_shared_setsBaseURL():
func test_shared_setsSession() {
// given
let session = URLSession.shared
// then
XCTAssertEqual(DogPatchClient.shared.session, session)
}
This test will evaluate the session property you created in shared. Build and run this test; you’ll see it fails because session on DogPatchClient.shared is not equal to URLSession.shared. To make it pass, open DogPatchClient.swift, and update the input parameter value for session to .shared. Build and run the tests again, and verify they all pass.
Finally, add the following test below test_shared_setsSession():
func test_shared_setsResponseQueue() {
// given
let responseQueue = DispatchQueue.main
// then
XCTAssertEqual(DogPatchClient.shared.responseQueue,
responseQueue)
}
This test will check the final property in the shared instance, responseQueue. Build and run this test, and you’ll see it fails because responseQueue on DogPatchClient.shared is currently set to nil. To fix this, open DogPatchClient.swift, and update the input parameter value for responseQueue to .main. Build and run the tests again to verify they all pass.
Ultimately, your static shared property inside DogPatchClient should look like this:
static let shared =
DogPatchClient(
baseURL: URL(string:
"https://dogpatchserver.herokuapp.com/api/v1/")!,
session: .shared,
responseQueue: .main)
Adding a network client property
Next, you need to add a networkClient property to ListingsViewController. Before you can write app code, of course, you need a failing test.
Open ListingsViewControllerTests.swift and add the following right after // MARK: - Instance Properties - Tests:
func test_networkClient_setToDogPatchClient() {
XCTAssertTrue(sut.networkClient === DogPatchClient.shared)
}
You assert that sut.networkClient has pointer equality to DogPatchClient.shared. Since you haven’t defined networkClient on DogPatchClient, this test won’t compile yet.
To fix this, open ListingsViewController.swift and add the following property right after // MARK: - Instance Properties:
var networkClient =
DogPatchClient(baseURL: URL(string: "http://example.com")!,
session: URLSession(),
responseQueue: nil)
You declare this as a var to allow your tests to replace it with a mock object later on. By defining this property, you’ve also fixed the compiler error.
Build and run the unit tests and you’ll see the last test fails because networkClient isn’t set to DogPatchClient .shared. To make it pass, replace the declaration for var networkClient inside ListingsViewController.swift with the following:
var networkClient = DogPatchClient.shared
Build and run your tests again to verify they all pass.
Using the network client
While you could use DogPatchClient directly in your unit tests, this has several drawbacks:
- You’d make real network calls, which would require an internet connection.
- The network calls would fail if an internet connection wasn’t available or the server was down.
- You wouldn’t be able to predict the network response in advance, so you couldn’t verify the values are what you expected.
- Your unit tests would be slow to run because each would need to wait for a network response.
Fortunately, there’s a better option: Use a mock network client. This lets you avoid making real network calls while completely controlling the response results.
There are two ways you can create a mock network client in Swift:
-
You can create a mock by subclassing
DogPatchClientand overriding each of its methods. This works, but you may accidentally make real network calls if you forget to override a method. You may also cause side effects, such as caching fake network responses. -
You can create a network client protocol and depend on this instead of
DogPatchClientdirectly. You can create a mock object by implementing it. In turn, you eliminate the possibility of making real network calls or causing side effects. Nice! The main downside of this approach is that you must create an extra type for the protocol. However, this is usually very quick and easy to do.
In general, you should create a mock network client using a protocol instead of subclassing-and-overriding.
One reason you might choose to subclass-and-override over creating a protocol is if your app is tightly coupled to the network client or its related types. Even then, you should strive to replace it with a protocol in the long term.
Creating the network client protocol
What should you put in the network client protocol? Any methods and properties that consumers need to use! In turn, you’ll be able to use your mock to validate that you’re calling these correctly.
Okay, that’s enough theory! You’re ready to TDD the protocol now.
As always, you’ll write a test first. Open DogPatchClientTests.swift and add the following method, right before test_shared_setsBaseURL():
func test_conformsTo_DogPatchService() {
XCTAssertTrue((sut as AnyObject) is DogPatchService)
}
Here’s how this works, from the inside out:
- First, you cast
sut as AnyObjectto prevent a compiler warning later. - Finally, you assert
sut is DogPatchService.
This currently causes a compiler error because you haven’t defined DogPatchService. To fix this, open DogPatchClient.swift and add the following, right before the class declaration:
protocol DogPatchService {
}
Build and run the unit tests and, as expected, the last test will fail. To make it pass, add the following to the end of DogPatchClient.swift, after the closing class curly brace:
extension DogPatchClient: DogPatchService { }
Build and run the tests again, and verify they all pass.
This protocol isn’t very useful yet because it doesn’t have any methods. For this, add the following test below test_conformsTo_DogPatchService():
func test_dogPatchService_declaresGetDogs() {
// given
let service = sut as DogPatchService
// then
_ = service.getDogs() { _, _ in }
}
This test won’t compile because DogPatchService doesn’t know anything about getDogs. To fix this, adding the following inside the DogPatchService protocol:
func getDogs(completion:
@escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask
Build and run your tests now, and they should all pass.
Are there any other properties or methods you should add to DogPatchService? For example, what about init(baseURL:session:responseQueue:) or the shared property?
No, you don’t need to add these because they are implementation details. A consumer doesn’t need to know how you constructed its dependency; they only need to know the behavior the dependency provides. This, in turn, defines which methods and properties go into the protocol.
For now, this one method is all you need in DogPatchService!
Creating the mock network client
You now need to create the mock network client. Your first step is to write a test for… Oh, wait! You don’t need a test. ;]
Your mock network client won’t be part of your production code. It enables you to write unit tests, and this, in turn, enables you to write app code. Okay, carry on then…!
Within DogPatchTests/Test Types, create a new group for Mocks and create a new Swift File called MockDogPatchService.swift within it. Then, right-click on Test Types and select Sort by Name.
Your file hierarchy for DogPatchTests should look like this:
Replace the contents of MockDogPatchService.swift with the following:
// 1
@testable import DogPatch
import Foundation
// 2
class MockDogPatchService: DogPatchService {
// 3
var getDogsCallCount = 0
var getDogsDataTask = URLSessionDataTask()
var getDogsCompletion: (([Dog]?, Error?) -> Void)!
// 4
func getDogs(
completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask {
getDogsCallCount += 1
getDogsCompletion = completion
return getDogsDataTask
}
}
Here’s what you’ve done:
- You declare
@testable import DogPatchto import all of the public and internal types fromDogPatch. You also importFoundation, which includesURLSessionDataTask. - You create a new type for
MockDogPatchServicethat conforms toDogPatchService. - You add properties for
getDogsCallCount,getDogsDataTaskandgetDogsCompletion. You’ll use them to verify the mock gets called as expected, and to return stubbed responses. - You implement
getDogs(completion:), whichDogPatchServicerequires. Whenever it’s called, you incrementgetDogsCallCount, setgetDogsCompletionand returngetDogsDataTask.
Fantastic, you implemented this mock like a pro! It mirrors how DogPatchClient works, but it allows you to fully control the response that’s returned, doesn’t require a network connection and doesn’t have any network delay. So now, it’s time to put it to work.
Using the mock network client
You’re finally ready to use the mock network client!
Open ListingsViewControllerTests.swift, and you’ll see that several tests are included for you for the existing functionality. Your job is to do TDD for refreshData().
Your first test will assert that the view controller holds onto the returned data task. To do this, add the following code right after test_viewWillAppear_calls_refreshData():
func test_refreshData_setsRequest() {
// given
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
}
Here, you create mockNetworkClient and attempt to set this as sut.networkClient. Unfortunately, this causes a compiler error. What’s up with that?
Xcode actually gives a helpful error message:
Cannot assign value of type 'MockDogPatchService' to type 'DogPatchClient'
The compiler expects networkClient to be of type DogPatchClient, yet you’re attempting to set it to MockDogPatchClient, which doesn’t inherit from DogPatchClient. To fix this error, you need to explicitly set the type of networkClient to be DogPatchService.
Replace this code within ListingsViewController:
var networkClient = DogPatchClient.shared
With this instead:
var networkClient: DogPatchService = DogPatchClient.shared
Both MockDogPatchService and DogPatchClient conform to DogPatchService, so this eliminates the compiler error. However, you’ll notice that test_networkClient_setToDogPatchClient no longer compiles because Swift cannot use the identical-to operator, ===, to compare DogPatchClient and DogPatchService. To fix this, you need to cast the protocol type to the object type you want to compare. Replace the contents of test_networkClient_setToDogPatchClient with:
XCTAssertTrue((sut.networkClient as? DogPatchClient)
=== DogPatchClient.shared)
Run your tests and they should all pass again.
Now, add this next code within test_refreshData_setsRequest(), right before its closing method brace:
// when
sut.refreshData()
// then
XCTAssertEqual(sut.dataTask, mockNetworkClient.getDogsDataTask)
Since you haven’t declared dataTask on ListingsViewController, this doesn’t compile. To fix this, open ListingsViewController.swift and add the following right after var viewModels:
var dataTask: URLSessionDataTask?
This fixes the compiler error, so build and run the tests and verify that it fails. To make it pass, you need to set dataTask whenever refreshData() is called.
Replace the // TODO: - Write this comment inside refreshData() with the following:
dataTask = networkClient.getDogs() { dogs, error in
}
Build and run the tests again, and they’ll all pass.
It’s possible that this code will call refreshData more than once in quick succession. For example, this could happen if the user “pulls to refresh” when a network call is already in progress.
However, if dataTask is already set, you don’t want to call getDogs multiple times. Add the following test to ensure you’re only calling getDogs once, even if refreshData is called in quick succession below test_refreshData_setsRequest():
func test_refreshData_ifAlreadyRefreshing_doesntCallAgain() {
// given
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
// when
sut.refreshData()
sut.refreshData()
// then
XCTAssertEqual(mockNetworkClient.getDogsCallCount, 1)
}
This test calls refreshData twice in succession to simulate that scenario.
Build and run this test to verify it fails. To make it pass, open ListingsViewController.swift and add the following code just after the opening curly brace for refreshData():
guard dataTask == nil else { return }
This guard returns early if dataTask is not nil. Build and run your unit tests, and they should all now pass.
Do you see anything that needs to be refactored? The app code looks fine, but what about the unit tests? Yep, you’ve duplicated the code for setting sut.networkClient to mockNetworkClient.
To eliminate this duplication, first add this new property right after the var sut line:
var mockNetworkClient: MockDogPatchService!
Next, add the following method right after the givenDogs(count:) method:
func givenMockNetworkClient() {
mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
}
Then add the following within tearDown(), right after its opening method brace:
mockNetworkClient = nil
This ensures mockNetworkClient is set to nil after each test run completes.
Finally, replace the following two lines in both test_refreshData_setsRequest and test_refreshData_ifAlreadyRefreshing_doesntCallAgain:
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
With this one line instead:
givenMockNetworkClient()
This gets rid of the duplicate code. Now, build and run the tests to verify each still passes.
For the next test, you need to ensure that you set dataTask back to nil once the completion is called for getDogs. Add the following test below test_refreshData_ifAlreadyRefreshing_doesntCallAgain():
func test_refreshData_completionNilsDataTask() {
// given
// 1
givenMockNetworkClient()
let dogs = givenDogs()
// when
// 2
sut.refreshData()
// 3
mockNetworkClient.getDogsCompletion(dogs, nil)
// then
// 4
XCTAssertNil(sut.dataTask)
}
Here’s how this test works:
- You make excellent use of your helper methods to create
mockNetworkClientanddogs. - You then call
sut.refreshData()to set thedataTask. - You pass
dogsto thegetDogsCompletionclosure on themockNetworkClient. This executes the passed-in closure fromListingsViewControllerand should set thedataTasktonil. - You assert that the
sut.dataTaskis actuallynil.
Build and run this test, and you’ll see it fails. Of course, that’s because you haven’t actually set dataTask to nil within the getDogs completion closure.
To make this pass, add this line right inside the completion closure within refreshData on ListingsViewController:
self.dataTask = nil
Build and run the tests to verify the last one now passes.
You’re now finally ready to test the “happy path”, which returns dogs successfully and sets it on the ListingsViewController. Add the following test below test_refreshData_completionNilsDataTask():
func test_refreshData_givenDogsResponse_setsViewModels() {
// given
// 1
givenMockNetworkClient()
let dogs = givenDogs()
let viewModels = dogs.map { DogViewModel(dog: $0) }
// when
// 2
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)
// then
// 3
XCTAssertEqual(sut.viewModels, viewModels)
}
Here’s how this test works:
- First, you use your helper methods to create
mockNetworkClientanddogs, then you createviewModelsby mapping eachdogto aDogViewModel. - Next, you call
sut.refreshData()and execute thegetDogsCompletionwith the givendogs. - Finally, you assert that
sut.viewModelsis equal toviewModels.
Build and run this test to verify it fails. You need to set viewModels on ListingsViewController to make it pass.
Add the following right after dataTask = nil within the refreshData() on ListingsViewController:
self.viewModels = dogs?.map { DogViewModel(dog: $0) } ?? []
This likewise calls map to turn dogs into a DogViewModel array. If there’s an error, dogs might be nil so you use the optional unwrap operator ? and provide the default value as an empty array.
Build and run your tests, and you’ll see this last one now passes.
Is there anything more to refactor here? Well, maybe…
At first glance, the code between test_refreshData_completionNilsDataTask and test_refreshData_completionNilsDataTask looks similar. However, you’ve already factored out several helper methods, but you’re using them here.
You could try to refactor these tests further, but you’d likely make them harder to understand. Consequently, it’s okay to leave them as is!
For the next test, you need to actually reload the tableView after the viewModels are set. Add the following test below test_refreshData_givenDogsResponse_setsViewModels():
func test_refreshData_givenDogsResponse_reloadsTableView() {
// given
givenMockNetworkClient()
let dogs = givenDogs()
// 1
class MockTableView: UITableView {
var calledReloadData = false
override func reloadData() {
calledReloadData = true
}
}
// 2
let mockTableView = MockTableView()
sut.tableView = mockTableView
// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)
// then
// 3
XCTAssertTrue(mockTableView.calledReloadData)
}
There are three significant parts to this test:
- First, you create a
MockTableViewto overridereloadData(). Inside that, you update a Boolean forcalledReloadData. - Next, you create a new instance for
mockTableViewand set this assut.tableViewto ensure it’s used. - Finally, after you’ve called
refreshData()and executed thegetDogsCompletion, you assert thatmockTableView.calledReloadDatais true.
Build and run the unit tests, and you’ll see this test fails because you don’t currently call reloadData on the tableView. To get this to pass, add the following line, right after setting viewModels within refreshData() on ListingsViewController:
self.tableView.reloadData()
Build and run the tests again, and this last one will now pass.
Okay, you’re finally ready to check out the app. Build and run it! You’ll see that the dogs… don’t show?!
Instead, the view controller shows an error screen, and if you “pull down to refresh,” you’ll see the “loading indicator” never disappears.
This is because of the way that you implemented tableView(_:numberOfRowsInSection:): It considers whether or not the tableView is refreshing.
To fix this, you need to begin and end refreshing on the table view’s refreshControl. Add the following test below test_refreshData_givenDogsResponse_reloadsTableView():
func test_refreshData_beginsRefreshing() {
// given
givenMockNetworkClient()
// when
sut.refreshData()
// then
XCTAssertTrue(sut.tableView.refreshControl!.isRefreshing)
}
This test verifies that isRefreshing on the refreshControl is true after calling refreshData(). Build and run this test, and it will fail because you haven’t started refreshing yet.
To fix this, add the following line right after the guard statement within refreshData() on ListingsViewController:
self.tableView.refreshControl?.beginRefreshing()
Build and run your test again, and this will now pass.
Lastly, you need to end refreshing whenever your code calls the completion closure. Add the following test to verify this below test_refreshData_beginsRefreshing():
func test_refreshData_givenDogsResponse_endsRefreshing() {
// given
givenMockNetworkClient()
let dogs = givenDogs()
// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)
// then
XCTAssertFalse(sut.tableView.refreshControl!.isRefreshing)
}
This test calls refreshData(), executes the getDogsCompletion closure and asserts that isRefreshing on the refreshControl is false. Build and run this test, and it will fail because you haven’t actually finished refreshing yet.
To make it pass, add the following line right after setting viewModels within refreshData() on ListingsViewController:
self.tableView.refreshControl?.endRefreshing()
Build and run your tests again, and they should all pass.
Great job! You’ve done TDD for the entire refreshData() implementation. Build and run the app to see it in action.
Key points
In this chapter, you learned how to TDD using a network client. Here are the key points you covered:
- You created a
sharedinstance for the network client to avoid having multiple instances throughout the app. - You avoided using the real network client directly in your unit tests since that would require an internet connection, which would cause them to be slower and make testing responses harder. You used a mock network client instead.
- You learned why it’s better to create a mock network client by implementing a protocol, instead of subclassing and overriding. By doing so, you avoided accidentally making real network calls and side effects such as caching.
You’re now able to display network results on screen! Wouldn’t it be nice if you could also see the images of the pups instead of just a placeholder image? You bet it would! In the next chapter, you’ll learn how to create an image client to help you do just that.