Developers that are new to unit testing often wonder what they should unit test. Unit testing is a type of black-box testing, which means that you don't care how the entity under test does what it does. From the moment you start writing unit tests, you need to stop being a developer and take on the mindset of a tester. What do I mean by that?

As a tester, you're not interested in how the entity under test works. You carefully inspect the interface of the entity and make sure the entity behaves as specified by the interface. Suppose that you're responsible for testing a car. The moment you push the gas pedal, you expect the car to move forward. As a tester, you're not interested in how the car moves forward. The technical details are different for a gas car and an electric car, but those technical details are not important. The specification says that pushing the gas pedal should move the car forward. That is what you test. As a tester, you test the behavior and functionality of the entity under test.

Writing Unit Tests

If we apply this mindset to the RootViewModel class, then it immediately becomes clear that there isn't a lot we need to unit test. Let's take a look at the interface of the RootViewModel class. Open the Assistant Editor on the right and choose Generated Interface > RootViewModel.swift from the list of options.

Showing the Generated Interface of the RootViewModel Class

import Foundation

internal class RootViewModel : NSObject {

    internal enum WeatherDataResult {

        case success(WeatherData)

        case failure(WeatherDataError)
    }

    internal enum WeatherDataError : Error {

        case notAuthorizedToRequestLocation

        case failedToRequestLocation

        case noWeatherDataAvailable
    }

    internal typealias FetchWeatherDataCompletion = (WeatherDataResult) -> Void

    internal var didFetchWeatherData: FetchWeatherDataCompletion?

    internal init(networkService: NetworkService, locationService: LocationService)

    internal func refresh()
}

The only methods the RootViewModel class exposes are its designated initializer and the refresh() method. I usually only unit test initializers that are failable. That's a personal choice. This means that we only need to unit test the refresh() method.

Open RootViewModelTests.swift and create a method for the unit test, testRefresh(). In the unit test, we invoke the method under test, refresh().

func testRefresh() {
    // Invoke Method Under Test
    viewModel.refresh()
}

How do we unit test the refresh() method? This is simpler than you might think. We need to ask the question "What do we expect to happen when the refresh() method is invoked?" The application should fetch the location of the device, fetch weather data for that location, and invoke the didFetchWeatherData handler.

The idea is straightforward. We assign a closure to the didFetchWeatherData handler. That closure needs to be invoked for the unit test to pass. Let me show you how that works. We assign a value to the didFetchWeatherData property, a closure that accepts one argument of type WeatherDataResult.

func testRefresh() {
    // Install Handler
    viewModel.didFetchWeatherData = { (result) in

    }

    // Invoke Method Under Test
    viewModel.refresh()
}

We use pattern matching to access the WeatherData instance that is stored in the WeatherDataResult instance.

func testRefresh() {
    // Install Handler
    viewModel.didFetchWeatherData = { (result) in
        if case .success(let weatherData) = result {

        }
    }

    // Invoke Method Under Test
    viewModel.refresh()
}

Let's inspect the WeatherData instance before we write any assertions. Add a print statement to the if clause of the if statement.

func testRefresh() {
    // Install Handler
    viewModel.didFetchWeatherData = { (result) in
        if case .success(let weatherData) = result {
            print(weatherData)
        }
    }

    // Invoke Method Under Test
    viewModel.refresh()
}

Set the destination to an iPhone or iPad simulator. Run the unit test for the RootViewModel class by clicking the diamond next to the declaration of the RootViewModelTests class. Open the console at the bottom to inspect the output. It should look something like this.

Test Suite 'Selected tests' started at 2018-12-13 15:42:49.411
Test Suite 'RainstormTests.xctest' started at 2018-12-13 15:42:49.411
Test Suite 'RootViewModelTests' started at 2018-12-13 15:42:49.411
Test Case '-[RainstormTests.RootViewModelTests testRefresh]' started.
Test Case '-[RainstormTests.RootViewModelTests testRefresh]' passed (0.002 seconds).
Test Suite 'RootViewModelTests' passed at 2018-12-13 15:42:49.414.
     Executed 1 test, with 0 failures (0 unexpected) in 0.002 (0.003) seconds
Test Suite 'RainstormTests.xctest' passed at 2018-12-13 15:42:49.414.
     Executed 1 test, with 0 failures (0 unexpected) in 0.002 (0.003) seconds
Test Suite 'Selected tests' passed at 2018-12-13 15:42:49.415.
     Executed 1 test, with 0 failures (0 unexpected) in 0.002 (0.004) seconds

There's good news and there's less good news. The good news is that the output in the console shows us that the unit test passed. The less good news is that the value of the WeatherData instance isn't printed to the console. How is that possible?

The unit test is more complex than it appears. Two events take place when the refresh() method is invoked, (1) the RootViewModel instance fetches the location of the device and (2) it fetches weather data for that location.

Fetching the location of the device is handled by the MockLocationService instance. That is something we carefully control. Fetching weather data, however, is handled by the URLSession API. The application sends a request to the Dark Sky API and waits for the response.

As I explained earlier, fetching the location of the device and fetching weather data are asynchronous operations. A unit test, however, runs synchronously, which means that the unit test has finished executing before the response of the Dark Sky API has arrived. The output in the console confirms this. The didFetchWeatherData handler isn't invoked and, as a result, the WeatherData instance isn't printed to the console.

Solving this problem isn't difficult. A few years ago, Apple added support for unit testing asynchronous operations. Let me show you how this works.

We define an expectation by creating an instance of the XCTestExpectation class. The initializer accepts one argument, a description of the expectation. The description is included in the test log and it's useful to debug any issues that may arise.

func testRefresh() {
    // Define Expectation
    let expectation = XCTestExpectation(description: "Fetch Weather Data")

    // Install Handler
    viewModel.didFetchWeatherData = { (result) in
        if case .success(let weatherData) = result {
            print(weatherData)
        }
    }

    // Invoke Method Under Test
    viewModel.refresh()
}

For the unit test to pass every expectation it defines needs to be fulfilled. The unit test waits for each expectation to be fulfilled by invoking the wait(for:timeout:) method. It accept two arguments, (1) an array of expectations that need to be fulfilled and (2) a timeout. If the expectations are not fulfilled within the time interval defined by the timeout, the unit test fails.

func testRefresh() {
    // Define Expectation
    let expectation = XCTestExpectation(description: "Fetch Weather Data")

    // Install Handler
    viewModel.didFetchWeatherData = { (result) in
        if case .success(let weatherData) = result {
            print(weatherData)
        }
    }

    // Invoke Method Under Test
    viewModel.refresh()

    // Wait for Expectation to Be Fulfilled
    wait(for: [expectation], timeout: 2.0)
}

An expectation is fulfilled when its fulfill() method is invoked. We fulfill the expectation in the if clause of the if statement.

func testRefresh() {
    // Define Expectation
    let expectation = XCTestExpectation(description: "Fetch Weather Data")

    // Install Handler
    viewModel.didFetchWeatherData = { (result) in
        if case .success(let weatherData) = result {
            print(weatherData)

            // 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 one more time and inspect the output in Xcode's console. It should look something like this.

Test Suite 'Selected tests' started at 2018-12-13 15:51:58.638
Test Suite 'RainstormTests.xctest' started at 2018-12-13 15:51:58.639
Test Suite 'RootViewModelTests' started at 2018-12-13 15:51:58.639
Test Case '-[RainstormTests.RootViewModelTests testRefresh]' started.
Status Code: 200
DarkSkyResponse(latitude: 0.0, longitude: 0.0, daily: Rainstorm.DarkSkyResponse.Daily(data: [Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-13 00:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 7.24, temperatureMin: 79.15, temperatureMax: 81.69), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-14 00:00:00 +0000, icon: "partly-cloudy-night", windSpeed: 10.13, temperatureMin: 79.19, temperatureMax: 81.37), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-15 00:00:00 +0000, icon: "rain", windSpeed: 9.45, temperatureMin: 78.81, temperatureMax: 81.82), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-16 00:00:00 +0000, icon: "partly-cloudy-night", windSpeed: 9.17, temperatureMin: 78.88, temperatureMax: 81.06), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-17 00:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 8.94, temperatureMin: 78.3, temperatureMax: 80.79), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-18 00:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 9.77, temperatureMin: 78.0, temperatureMax: 81.64), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-19 00:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 9.39, temperatureMin: 78.72, temperatureMax: 81.4), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2018-12-20 00:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 8.77, temperatureMin: 78.91, temperatureMax: 81.96)]), currently: Rainstorm.DarkSkyResponse.Conditions(time: 2018-12-13 14:51:59 +0000, icon: "clear-day", summary: "Humid", windSpeed: 8.01, temperature: 81.69))
Test Case '-[RainstormTests.RootViewModelTests testRefresh]' passed (0.716 seconds).
Test Suite 'RootViewModelTests' passed at 2018-12-13 15:51:59.356.
     Executed 1 test, with 0 failures (0 unexpected) in 0.716 (0.716) seconds
Test Suite 'RainstormTests.xctest' passed at 2018-12-13 15:51:59.356.
     Executed 1 test, with 0 failures (0 unexpected) in 0.716 (0.717) seconds
Test Suite 'Selected tests' passed at 2018-12-13 15:51:59.356.
     Executed 1 test, with 0 failures (0 unexpected) in 0.716 (0.718) seconds

The unit test passes and the value of the WeatherData instance is printed to the console, confirming that the didFetchWeatherData handler was invoked after successfully fetching weather data from the Dark Sky API. You have successfully implemented your first asynchronous unit test.

What's Next?

But we're not quite done yet. I warned you that unit testing the RootViewModel class is a bit more complex. What happens if the Dark Sky API isn't available? What happens if the machine that runs the test suite has a poor network connection? In both scenarios the test suite fails. We need to guard against these scenarios. That is the focus of the next episode.