We're currently only unit testing the happy path. In this episode, we expand the test suite and I show you how to simulate failures. It isn't easy to accurately test failures in the real world and that emphasizes the importance of a robust test suite. Let me show you what I have in mind.
Unit Testing Failures
We need to unit test two types of failures. Fetching the location of the device can fail and fetching weather data can fail. Let's start with the former.
Fetching the Location of the Device
To unit test the failure of fetching the location of the device we need to revisit the implementation of the MockLocationService class. The current implementation only supports the happy path. Adding support for failure isn't difficult. Open MockLocationService.swift and change the type of the location property to an optional type.
import Foundation
@testable import Rainstorm
class MockLocationService: LocationService {
// MARK: - Properties
var location: Location? = Location(latitude: 0.0, longitude: 0.0)
...
}
If the location property doesn't have a value, then fetching the location of the device should fail. Let's update the implementation of the fetchLocation(completion:) method to translate that into code. We define a constant, result, of type LocationServiceResult. We safely unwrap the value stored in the location property and create an instance of the LocationServiceResult enum. If the location property doesn't have a value, then fetching the location of the device fails.
func fetchLocation(completion: @escaping LocationService.FetchLocationCompletion) {
// Create Result
let result: LocationServiceResult
if let location = location {
result = .success(location)
} else {
result = .failure(.notAuthorizedToRequestLocation)
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// Invoke Handler
completion(result)
}
}
These simple changes enable us to write a unit test for the scenario in which fetching the location of the device fails. Open RootViewModelTests.swift and rename the first unit test to testRefresh_Success().
func testRefresh_Success() {
...
}
Create another unit test and name it testRefresh_FailedToFetchLocation(). We can copy the implementation of the first unit test as a starting point.
func testRefresh_FailedToFetchLocation() {
// 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)
}
To configure the MockLocationService instance, we need to keep a reference to the instance. Create a property with name locationService in the RootViewModelTests class. The type of the property is MockLocationService!, an implicitly unwrapped optional.
import XCTest
@testable import Rainstorm
class RootViewModelTests: XCTestCase {
// MARK: - Properties
var viewModel: RootViewModel!
// MARK: -
var locationService: MockLocationService!
...
}
We create a MockLocationService instance in the setUp() method, assign it to the locationService property, and pass it to the initializer of the RootViewModel class.
override func setUp() {
super.setUp()
// Initialize Mock Network Service
let networkService = MockNetworkService()
// Configure Mock Network Service
networkService.data = loadStub(name: "darksky", extension: "json")
// Initialize Mock Location Service
locationService = MockLocationService()
// Initialize Root View Model
viewModel = RootViewModel(networkService: networkService, locationService: locationService)
}
Return to the testRefresh_FailedToFetchLocation() method and set the location property of the MockLocationService instance to nil to indicate that fetching the location of the device should fail. That's the first step.
func testRefresh_FailedToFetchLocation() {
// Configure Location Service
locationService.location = nil
// 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)
}
The result that is passed to the didFetchWeatherData handler should indicate that fetching the location of the device failed. We access the Error object of the WeatherDataResult instance through pattern matching. The Error object should be equal to RootViewModel.WeatherDataError.notAuthorizedToRequestLocation.
func testRefresh_FailedToFetchLocation() {
// Configure Location Service
locationService.location = nil
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.notAuthorizedToRequestLocation)
// 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 tests of the RootViewModel class to make sure they pass.

Open RootViewModel.swift. The code coverage data confirms that both the success and failure paths are covered by the unit tests. That's a good start.

Fetching Weather Data
We have three more unit tests to write. Open RootViewModel.swift and take a look at the implementation of the fetchWeatherData(for:) method. We need to cover the scenarios in which (1) the request fails, (2) the response is invalid, and (3) no data is returned and no error is returned. Let's start with the first scenario. The unit test is easy to implement thanks to the groundwork we laid earlier in this series.
Create a new unit test and name it testRefresh_FailedToFetchWeatherData_RequestFailed(). The method name doesn't look pretty, but it clearly describes which scenario we're covering in the unit test. We can copy the implementation of the first unit test as a starting point.
func testRefresh_FailedToFetchWeatherData_RequestFailed() {
// 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)
}
We need to configure the MockNetworkService instance to simulate the scenario in which the request for weather data fails. To configure the MockNetworkService instance, we need to keep a reference to the instance. Create a property with name networkService in the RootViewModelTests class. The type of the property is MockNetworkService!, an implicitly unwrapped optional.
import XCTest
@testable import Rainstorm
class RootViewModelTests: XCTestCase {
// MARK: - Properties
var viewModel: RootViewModel!
// MARK: -
var networkService: MockNetworkService!
var locationService: MockLocationService!
...
}
We assign the MockNetworkService instance in the setUp() to the networkService property and pass it to the initializer of the RootViewModel class. This means we only need to remove the let keyword.
override func setUp() {
super.setUp()
// Initialize Mock Network Service
networkService = MockNetworkService()
// Configure Mock Network Service
networkService.data = loadStub(name: "darksky", extension: "json")
// Initialize Mock Location Service
locationService = MockLocationService()
// Initialize Root View Model
viewModel = RootViewModel(networkService: networkService, locationService: locationService)
}
We can now reference the MockNetworkService instance in the unit test. To simulate a failed request, we need to assign a value to the error property of the MockNetworkService instance. We keep it simple. We create an NSError instance and assign it to the error property of the MockNetworkService instance.
func testRefresh_FailedToFetchWeatherData_RequestFailed() {
// Configure Network Service
networkService.error = NSError(domain: "com.cocoacasts.network.service", code: 1, userInfo: nil)
...
}
The result that is passed to the didFetchWeatherData handler should indicate that fetching weather data for the location of the device failed. We access the Error object of the WeatherDataResult instance through pattern matching. The Error object should be equal to RootViewModel.WeatherDataError.noWeatherDataAvailable.
func testRefresh_FailedToFetchWeatherData_RequestFailed() {
// Configure Network Service
networkService.error = NSError(domain: "com.cocoacasts.network.service", code: 1, userInfo: nil)
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.noWeatherDataAvailable)
// 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 tests of the RootViewModel class to make sure they pass.

To simulate the second scenario, the response of the Dark Sky API should be invalid. It shouldn't be possible to create a DarkSkyResponse instance by deserializing the response. This is easy to do.
Create a new unit test and name it testRefresh_FailedToFetchWeatherData_InvalidResponse(). We can copy the implementation of the previous unit test as a starting point.
func testRefresh_FailedToFetchWeatherData_InvalidResponse() {
// Configure Network Service
networkService.error = NSError(domain: "com.cocoacasts.network.service", code: 1, userInfo: nil)
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.noWeatherDataAvailable)
// Fulfill Expectation
expectation.fulfill()
}
}
// Invoke Method Under Test
viewModel.refresh()
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
In this unit test, we don't need to set the error property of the MockNetworkService instance. To simulate the failure, we need to set the data property of the MockNetworkService instance. The value should be a Data instance that cannot be used to create a DarkSkyResponse instance. This isn't difficult.
func testRefresh_FailedToFetchWeatherData_InvalidResponse() {
// Configure Network Service
networkService.data = "data".data(using: .utf8)
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.noWeatherDataAvailable)
// Fulfill Expectation
expectation.fulfill()
}
}
// Invoke Method Under Test
viewModel.refresh()
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
We create a string literal and encode it. That's it. We don't need to update the assertion in the didFetchWeatherData handler since the Error object should also be equal to RootViewModel.WeatherDataError.noWeatherDataAvailable. Run the unit tests of the RootViewModel class to make sure they pass.

The third and last scenario is similar. The difference is that we need to set the data property to nil. Create a new unit test, name it testRefresh_FailedToFetchWeatherData_NoErrorNoResponse(), and copy the implementation of the previous unit test as a starting point. In the unit test, we set the data property of the MockNetworkService instance to nil. That's the only change we need to make. The assertion doesn't need to change.
func testRefresh_FailedToFetchWeatherData_NoErrorNoResponse() {
// Configure Network Service
networkService.data = nil
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.noWeatherDataAvailable)
// 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 tests of the RootViewModel class to make sure they pass.

Refreshing Weather Data
Open RootViewModel.swift and inspect the code coverage data on the right. That looks much better, but we can still see a few gaps in the test suite. The code that hasn't been covered by the test suite is located in the setupNotificationHandling() method.
Remember that the application refreshes the weather data every time the application moves to the foreground. How do we unit test this feature? It isn't as difficult as you might think. We need to write three unit tests. Let's start with the first scenario.
No Timestamp
If the user defaults database doesn't contain a value for the key didFetchWeatherData, then the application should refresh the weather data. Create a new unit test, name it testApplicationWillEnterForeground_NoTimestamp(), and copy the implementation of the previous unit test as a staring point.
func testApplicationWillEnterForeground_NoTimestamp() {
// Configure Network Service
networkService.data = nil
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
if case .failure(let error) = result {
XCTAssertEqual(error, RootViewModel.WeatherDataError.noWeatherDataAvailable)
// Fulfill Expectation
expectation.fulfill()
}
}
// Invoke Method Under Test
viewModel.refresh()
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
We first need to make the unit test fail. We remove the configuration of the MockNetworkService instance and the refresh() invocation. If the application tried to fetch weather data when the application entered the foreground, then the didFetchWeatherData handler should be invoked. For this unit test, we're not interested in the result of the weather data request. We only want to make sure the didFetchWeatherData handler is invoked because that indicates that the application refreshed the weather data, successfully or unsuccessfully.
func testApplicationWillEnterForeground_NoTimestamp() {
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
Run the unit test to see the result. The unit test fails and that isn't a surprise. The refresh() method isn't invoked, which means that the didFetchWeatherData handler isn't invoked either. We need to simulate that the application enters the foreground by posting a notification with name UIApplicationWillEnterForeground.
func testApplicationWillEnterForeground_NoTimestamp() {
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
Before we run the unit test, it's important that we reset the user defaults database. In the next unit test, we modify the user defaults database. That is fine, but we need to avoid that other unit tests fail as a result. This unit test assumes that no value is set in the user defaults database for the didFetchWeatherData key.
func testApplicationWillEnterForeground_NoTimestamp() {
// Reset User Defaults
UserDefaults.standard.removeObject(forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
Run the unit test one more time. This time it should pass.
Should Refresh
The second unit test tests the scenario in which a timestamp is set in the user defaults database and the application should refresh the weather data. Create a new unit test, name it testApplicationWillEnterForeground_ShouldRefresh(), and copy the implementation of the previous unit test.
func testApplicationWillEnterForeground_ShouldRefresh() {
// Reset User Defaults
UserDefaults.standard.removeObject(forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
This time we set a value for the didFetchWeatherData key in the user defaults database. It needs to be far enough in the past to ensure that it doesn't cross the threshold we defined in the RootViewModel class. Run the unit test to make sure it passes.
func testApplicationWillEnterForeground_ShouldRefresh() {
// Reset User Defaults
UserDefaults.standard.set(Date().addingTimeInterval(-3600.0), forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
In the unit test, we modify the user defaults database. That's fine, but it's important that we reset the user defaults database after the unit test has finished executing. We do this in the tearDown() method of the RootViewModelTests class.
override func tearDown() {
super.tearDown()
// Reset User Defaults
UserDefaults.standard.removeObject(forKey: "didFetchWeatherData")
}
Open RootViewModel.swift. The RootViewModel class is fully covered with unit tests. But there's one scenario we didn't cover. The weather data shouldn't be refreshed if the timestamp that is set in the user defaults database crosses the threshold we defined in the RootViewModel class. This may seem difficult to unit test. Fortunately, the XCTest framework comes to the rescue.
Create a new unit test, name it testApplicationWillEnterForeground_ShouldNotRefresh(), and copy the implementation of the previous unit test.
func testApplicationWillEnterForeground_ShouldNotRefresh() {
// Reset User Defaults
UserDefaults.standard.set(Date().addingTimeInterval(-3600.0), forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
We need to make two changes. The timestamp stored in the user defaults database should cross the threshold we defined in the RootViewModel class. To accomplish that, we set the value for the didFetchWeatherData key to the current time and date.
func testApplicationWillEnterForeground_ShouldNotRefresh() {
// Reset User Defaults
UserDefaults.standard.set(Date(), forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
The second change we need to make is related to the expectation. For a unit test to pass, every expectation the unit test defines needs to be fulfilled. We can modify this behavior by setting the isInverted property of the XCTestExpectation instance to true. This means that the expectation should not be fulfilled and that is exactly what we want.
func testApplicationWillEnterForeground_ShouldNotRefresh() {
// Reset User Defaults
UserDefaults.standard.set(Date(), forKey: "didFetchWeatherData")
// Define Expectation
let expectation = XCTestExpectation(description: "Fetch Weather Data")
// Configure Expectation
expectation.isInverted = true
// Install Handler
viewModel.didFetchWeatherData = { (result) in
// Fulfill Expectation
expectation.fulfill()
}
// Post Notification
NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
// Wait for Expectation to Be Fulfilled
wait(for: [expectation], timeout: 2.0)
}
If the expectation isn't fulfilled after two seconds, then the unit test passes. Run the test suite one more time to make sure every unit test passes.

What's Next?
Unit testing the RootViewModel class was much more involved than unit testing the structs we covered earlier in this series. We covered a lot of ground, though.
In the next episode, I show you another technique to mock network requests. It's a very different approach than the one we applied so far.