This episode should look and feel familiar if you've watched the previous episodes because we are about to write unit tests for the WeekViewModel and WeekDayViewModel structs. Let's start with the WeekViewModel struct.

Writing Unit Test for the Week View Model

We laid most of the groundwork for this episode earlier in this series. This means we can focus on writing unit tests. We start by adding a new file to the Test Cases group of the RainstormTests target. Choose the Unit Test Case Class template from the iOS > Source section.

Creating a Unit Test Case Class

Name the XCTestCase subclass WeekViewModelTests.

Creating a Unit Test Case Class

Remove the examples and the comments in the setUp() and tearDown() methods.

import XCTest

class WeekViewModelTests: XCTestCase {

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

To access the WeekViewModel struct, we need to add an import statement for the Rainstorm module. We prefix the import statement with the testable attribute to gain access to the internal entities of the Rainstorm module.

import XCTest
@testable import Rainstorm

class WeekViewModelTests: XCTestCase {

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

To make writing the unit tests easier, we declare a property, viewModel, of type WeekViewModel. Remember from earlier in this series that we use an implicitly unwrapped optional for convenience.

import XCTest
@testable import Rainstorm

class WeekViewModelTests: XCTestCase {

    // MARK: - Properties

    var viewModel: WeekViewModel!

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

We initialize the view model in the setUp() method of the XCTestCase subclass. Because this method is run before every unit test, we have access to a fully initialized WeekViewModel instance in each unit test.

We load the stub we added to the RainstormTests target earlier in this series. We take advantage of the loadStub(name:extension:) method we implemented earlier. This shows how useful it can be to spend some time creating a foundation for a project.

override func setUp() {
    super.setUp()

    // Load Stub
    let data = loadStub(name: "darksky", extension: "json")
}

We initialize a JSONDecoder instance, set its dateDecodingStrategy property to secondsSince1970, and use it to turn the Data instance into a DarkSkyResponse instance. This should look familiar.

override func setUp() {
    super.setUp()

    // Load Stub
    let data = loadStub(name: "darksky", extension: "json")

    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    // Initialize Dark Sky Response
    let darkSkyResponse = try! decoder.decode(DarkSkyResponse.self, from: data)
}

To initialize a WeekViewModel instance, we pass an array of ForecastWeatherConditions objects to the initializer of the WeekViewModel struct. That array is stored in the forecast property of the DarkSkyResponse instance.

override func setUp() {
    super.setUp()

    // Load Stub
    let data = loadStub(name: "darksky", extension: "json")

    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    // Initialize Dark Sky Response
    let darkSkyResponse = try! decoder.decode(DarkSkyResponse.self, from: data)

    // Initialize View Model
    viewModel = WeekViewModel(weatherData: darkSkyResponse.forecast)
}

We first unit test the numberOfDays computed property of the WeekViewModel struct. The unit test is short and simple. We assert that the value of the numberOfDays computed property is equal to 8. Remember that we can keep the unit test this simple because we use a stub. In other words, we control the environment in which the unit test is run.

// MARK: - Tests for Number of Days

func testNumberOfDays() {
    XCTAssertEqual(viewModel.numberOfDays, 8)
}

The next unit test focuses on the viewModel(for:) method of the WeekViewModel struct. You can make the unit test as simple or as complex as you want. We ask the view model for a WeekDayViewModel instance by invoking the viewModel(for:) method, passing in 5 as the argument.

// MARK: - Tests for View Model for Index

func testViewModelForIndex() {
    let weekDayViewModel = viewModel.viewModel(for: 5)
}

We assert that the values of the day and date properties are equal to the values we expect. It's important to understand that we're not unit testing the WeekDayViewModel struct. We use the assertions to make sure the view model returns a WeekDayViewModel instance that is populated with the weather data we expect.

// MARK: - Tests for View Model for Index

func testViewModelForIndex() {
    let weekDayViewModel = viewModel.viewModel(for: 5)

    XCTAssertEqual(weekDayViewModel.day, "Sunday")
    XCTAssertEqual(weekDayViewModel.date, "September 2")
}

We could write one more unit test. What happens if the index that is passed to the viewModel(for:) method exceeds the number of days the view model has weather data for? Let's try it out. Pass 15 to the viewModel(for:) method instead of 5. Run the test suite to see the result.

// MARK: - Tests for View Model for Index

func testNumberOfDays() {
    let weekDayViewModel = viewModel.viewModel(for: 15)

    XCTAssertEqual(weekDayViewModel.day, "Sunday")
    XCTAssertEqual(weekDayViewModel.date, "September 2")
}

The debugger notifies us that the index we passed to the viewModel(for:) method is out of bounds and, as a result, a fatal error is thrown. How can we write a unit test for this scenario? The question should be Should we write a unit test for this scenario? and the answer is no.

Why is that? The practical answer is simple. The XCTest framework doesn't allow us to assert whether a fatal error is thrown. But there's a better answer. If a fatal error is thrown, the application is immediately terminated because it enters a state it wasn't designed for. Writing a unit test to assert that a fatal error is thrown isn't useful. Why is that? Is it useful to assert that the application is terminated? It isn't. If the viewModel(for:) method is given an index that is out of bounds, then we made a mistake we need to fix because that should never happen.

We have one other option. We can code more defensively. We could make sure that the index passed to the viewModel(for:) method isn't out of bounds. If it is, we return nil. This would also mean the viewModel(for:) method returns an optional. This is a healthy approach in scenarios in which the argument that is passed to the method is unpredictable or not under the application's control. We leave the implementation of the viewModel(for:) method untouched. It's fine as is.

Writing Unit Tests for the Week Day View Model

It's time to unit test the WeekDayViewModel struct. Add a new file to the Test Cases group of the RainstormTests target. Choose the Unit Test Case Class template from the iOS > Source section.

Creating a Unit Test Case Class

Name the XCTestCase subclass WeekDayViewModelTests.

Creating a Unit Test Case Class

Remove the examples and the comments in the setUp() and tearDown() methods.

import XCTest

class WeekDayViewModelTests: XCTestCase {

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

We add an import statement for the Rainstorm module and define a property, viewModel, of type WeekDayViewModel.

import XCTest
@testable import Rainstorm

class WeekDayViewModelTests: XCTestCase {

    // MARK: - Properties

    var viewModel: WeekDayViewModel!

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

The setUp() method of the WeekDayViewModelTests class looks similar to that of the WeekViewModelTests class. The only difference is the initialization of the WeekDayViewModel instance. We ask the DarkSkyResponse instance for the weather data for a particular day.

import XCTest
@testable import Rainstorm

class WeekDayViewModelTests: XCTestCase {

    // MARK: - Properties

    var viewModel: WeekDayViewModel!

    // MARK: - Set Up & Tear Down

    override func setUp() {
        super.setUp()

        // Load Stub
        let data = loadStub(name: "darksky", extension: "json")

        // Initialize JSON Decoder
        let decoder = JSONDecoder()

        // Configure JSON Decoder
        decoder.dateDecodingStrategy = .secondsSince1970

        // Initialize Dark Sky Response
        let darkSkyResponse = try! decoder.decode(DarkSkyResponse.self, from: data)

        // Initialize View Model
        viewModel = WeekDayViewModel(weatherData: darkSkyResponse.forecast[5])
    }

    override func tearDown() {
        super.tearDown()
    }

}

The unit tests for the WeekDayViewModel class also look familiar. They resemble those for the DayViewModel class. Pause the video for a moment and give it a try.

The unit tests don't introduce new concepts or challenges. This is what the unit tests should look like.

// MARK: - Tests for Day

func testDay() {
    XCTAssertEqual(viewModel.day, "Sunday")
}

// MARK: - Tests for Date

func testDate() {
    XCTAssertEqual(viewModel.date, "September 2")
}

// MARK: - Tests for Temperature

func testTemperature() {
    XCTAssertEqual(viewModel.temperature, "53.9 °F - 68.2 °F")
}

// MARK: - Tests for Wind Speed

func testWindSpeed() {
    XCTAssertEqual(viewModel.windSpeed, "5 MPH")
}

// MARK: - Tests for Image

func testImage() {
    let viewModelImage = viewModel.image
    let imageDataViewModel = UIImagePNGRepresentation(viewModelImage!)!
    let imageDataReference = UIImagePNGRepresentation(UIImage(named: "cloudy")!)!

    XCTAssertNotNil(viewModelImage)
    XCTAssertEqual(viewModelImage!.size.width, 45.0)
    XCTAssertEqual(viewModelImage!.size.height, 33.0)
    XCTAssertEqual(imageDataViewModel, imageDataReference)
}

Run the test suite to make sure the unit tests pass.

What's Next?

Writing unit tests for view models isn't difficult as long as you stick to the Model-View-ViewModel pattern. The view model shouldn't be aware of the view controller it is associated with. That is what makes view models easy to use and unit test.

It's equally important to control the environment the test suite runs in. By taking advantage of stubs, we carefully control the unit tests and that allows us to focus on unit testing the implementation of the view models.