We wrote the first unit test for the RootViewModel class in the previous episode. It's true that unit testing asynchronous code is more complex than unit testing synchronous code, but I hope that the previous episode has shown that it isn't that hard.
At the end of the episode, I highlighted several flaws the test suite currently suffers from. Every time the unit test of the RootViewModel class is executed, a request is sent to the Dark Sky API. It makes the test suite slow and unreliable. In this episode, we rid the test suite of these issues.
Creating a Robust Test Suite
The RootViewModel class fetches weather data from the Dark Sky API using a URLSessionDataTask instance. How the RootViewModel class obtains weather data is an implementation detail that is private to the class. Other objects don't and shouldn't know how the RootViewModel class fetches weather data for a location. The interface of the RootViewModel class doesn't expose this implementation detail. Only the RootViewModel class knows where weather data comes from. That's good and that isn't something we want to change.
But that doesn't mean that we don't or shouldn't modify this behavior when the test suite runs. It's important that we are in control of the test environment and this applies even more to asynchronous operations. It's frustrating to see a unit test fail randomly. A test suite that is integrated into a continuous integration setup causes the build to fail. That is good if a failed unit test exposes a flaw in the codebase, but it's frustrating if a failed unit test is the result of a fragile test suite. A test suite needs to be rock solid.
Being in Control
The goal of this episode is to control the data that is received by the application over the network. There are several options to accomplish this goal. We cover two popular options in this series.
The solution we start with is similar to the one we used to mock the Core Location framework. The plan is to inject an object that is responsible for performing the request into the RootViewModel instance. We pass that object to the initializer of the RootViewModel class. This is better known as initializer injection and identical to how the LocationService instance is injected into the RootViewModel instance.
Create a new Swift file in the Protocols group and name it NetworkService.swift.


Define a protocol and name it NetworkService. To keep the implementation simple and the changes limited, I'd like to keep the interface similar to that of the URLSession API.
import Foundation
protocol NetworkService {
}
We first define a type alias, FetchDataCompletion, for a closure that accept three arguments, an optional Data instance, an optional URLResponse instance, and an optional Error instance. The closure returns Void.
import Foundation
protocol NetworkService {
// MARK: - Type Aliases
typealias FetchDataCompletion = (Data?, URLResponse?, Error?) -> Void
}
The protocol defines one method, fetchData(with url: URL, completionHandler: @escaping FetchDataCompletion). The method accepts a URL instance and an escaping closure of type FetchDataCompletion.
import Foundation
protocol NetworkService {
// MARK: - Type Aliases
typealias FetchDataCompletion = (Data?, URLResponse?, Error?) -> Void
// MARK: - Methods
func fetchData(with url: URL, completionHandler: @escaping FetchDataCompletion)
}
Defining the NetworkService protocol is only the first step. The second step is creating and implementing a type that conforms to the NetworkService protocol. Create a new Swift file in the Managers group and name it NetworkManager.swift.


Define a class, NetworkManager, that conforms to the NetworkService protocol.
import Foundation
class NetworkManager: NetworkService {
}
We need to implement one method, fetchData(with:completionHandler:). Because we take advantage of the URLSession API, the implementation is very simple. We ask the shared URLSession object for a URLSessionDataTask instance by passing the URL instance and the completion handler to the dataTask(with:completionHandler:) method of the URLSession class. We need to invoke resume() on the URLSessionDataTask instance to send the request.
import Foundation
class NetworkManager: NetworkService {
// MARK: - Network Service
func fetchData(with url: URL, completionHandler: @escaping NetworkService.FetchDataCompletion) {
URLSession.shared.dataTask(with: url, completionHandler: completionHandler).resume()
}
}
Let's put the NetworkManager class to use. Revisit RootViewModel.swift and define a private, constant property, networkService, of type NetworkService.
import Foundation
class RootViewModel: NSObject {
...
// MARK: -
private let networkService: NetworkService
private let locationService: LocationService
...
}
We also update the initializer of the RootViewModel class. The first argument of the initializer is an object that conforms to the NetworkService protocol. The value is stored in the networkService property.
// MARK: - Initialization
init(networkService: NetworkService, locationService: LocationService) {
// Set Services
self.networkService = networkService
self.locationService = locationService
super.init()
// Setup Notification Handling
setupNotificationHandling()
}
We use the NetworkService instance in the fetchWeatherData(for:) method. We replace any references to the URLSession API with the NetworkService instance. The change is small because we kept the API of the NetworkService protocol close to that of the URLSession API.
private func fetchWeatherData(for location: Location) {
// Initialize Weather Request
let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: location)
// Fetch Weather Data
networkService.fetchData(with: weatherRequest.url) { [weak self] (data, response, error) in
...
}
}
Before we can test the implementation, we need to update the implementation of the AppDelegate class. In application(_:didFinishLaunchingWithOptions:), we pass an instance of the NetworkManager class to the initializer of the RootViewModel class.
// MARK: - Application Life Cycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
guard let rootViewController = window?.rootViewController as? RootViewController else {
fatalError("Unexpected Root View Controller")
}
// Initialize Root View Model
let rootViewModel = RootViewModel(networkService: NetworkManager(), locationService: LocationManager())
// Configure Root View Controller
rootViewController.viewModel = rootViewModel
return true
}
It's time to test the changes we made. Build and run the application. Its user interface and behavior shouldn't have changed.
Mocking Network Requests
The changes we made allow us to mock the requests that the application sends to the Dark Sky API. Let's put that to the test. Create a new Swift file in the Mocks group and and name it MockNetworkService.swift.


Add an import statement for the Rainstorm module and define a class, MockNetworkService, that conforms to the NetworkService protocol.
import Foundation
@testable import Rainstorm
class MockNetworkService: NetworkService {
}
We define three variable properties, data of type Data?, error of type Error?, and statusCode of type Int. The default value of the statusCode property is set to 200.
import Foundation
@testable import Rainstorm
class MockNetworkService: NetworkService {
// MARK: - Properties
var data: Data?
var error: Error?
var statusCode: Int = 200
}
The last step is conforming the MockNetworkService class to the NetworkSerivce protocol, which means implementing the fetchData(with:completionHandler:) method. We create an instance of the HTTPURLResponse class, passing in the url parameter and the value stored in the statusCode property. We then invoke the completion handler of the fetchData(with:completionHandler:) method, passing in the values stored in the data and error properties as well as the HTTPURLResponse instance.
// MARK: - Network Service
func fetchData(with url: URL, completionHandler: @escaping NetworkService.FetchDataCompletion) {
// Create Response
let response = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)
// Invoke Handler
completionHandler(data, response, error)
}
By creating and implementing the MockNetworkService class, we are back in control of the test environment. Let me show you what that means. Revisit RootViewModelTests.swift and create an instance of the MockNetworkService class in the setUp() method.
// MARK: - Set Up & Tear Down
override func setUp() {
super.setUp()
// Initialize Mock Network Service
let networkService = MockNetworkService()
// Initialize Root View Model
viewModel = RootViewModel(locationService: MockLocationService())
}
We load the stub we created earlier in this series and assign it to the data property of the MockNetworkService instance.
// MARK: - Set Up & Tear Down
override func setUp() {
super.setUp()
// Initialize Mock Network Service
let networkService = MockNetworkService()
// Configure Mock Network Service
networkService.data = loadStub(name: "darksky", extension: "json")
// Initialize Root View Model
viewModel = RootViewModel(locationService: MockLocationService())
}
Because the MockNetworkService class conforms to the NetworkService protocol, we can pass it to the initializer of the RootViewModel class.
// MARK: - Set Up & Tear Down
override func setUp() {
super.setUp()
// Initialize Mock Network Service
let networkService = MockNetworkService()
// Configure Mock Network Service
networkService.data = loadStub(name: "darksky", extension: "json")
// Initialize Root View Model
viewModel = RootViewModel(networkService: networkService, locationService: MockLocationService())
}
To test the changes we made, we add two assertions to the if clause of the if statement. The MockLocationService instance returns a location with latitude 0.0 and longitude 0.0. Without the MockNetworkService class, the application would fetch weather data for that location by sending a request to the Dark Sky API. With the MockNetworkService class in place, the coordinates of the WeatherData instance are equal to those defined in the stub included in the test bundle.
func testRefresh() {
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .success(let weatherData) = result {
XCTAssertEqual(weatherData.latitude, 37.8267)
XCTAssertEqual(weatherData.longitude, -122.4233)
// Fulfill Expectation
expectation.fulfill()
}
}
// Invoke Method Under Test
viewModel.refresh()
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
Run the unit test for the RootViewModel class one more time to see the result. The passing of the unit test confirms that we control the test environment.
What's Next?
In this and the previous episodes, we built a simple but powerful foundation to work with. In the next episodes, we complete the unit tests for the RootViewModel class. We expand the test suite and collect code coverage data to ensure that the RootViewModel class is completely covered by the test suite.