Writing units tests for the view models of the WeekViewController class is just as easy as writing unit tests for the DayViewModel struct. We start with the unit tests for the WeekViewModel struct.

Unit Testing the Week View Model

Create a new XCTestCase subclass and name it WeekViewModelTests.swift.

Creating WeekViewModelTests.swift

The approach we take in this episode is identical to the approach we took in the previous episode. Add an import statement for the Cloudy module and define a property for the view model. The viewModel property is an implicitly unwrapped optional.

WeekViewModelTests.swift

import XCTest
@testable import Cloudy

class WeekViewModelTests: XCTestCase {

    // MARK: - Properties

    var viewModel: WeekViewModel!

    // MARK: - Set Up & Tear Down

    override func setUpWithError() throws {

    }

    override func tearDownWithError() throws {

    }

}

In the setUpWithError() method, we load the stub from the unit testing bundle and convert it to a WeatherData object. We use the value of the dailyData property to create a WeekViewModel object. Remember that the dailyData property is of type [WeatherDayData].

WeekViewModelTests.swift

override func setUpWithError() throws {
    // Load Stub
    let data = loadStub(name: "weather", extension: "json")

    // Create JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    // Decode JSON
    let weatherData = try decoder.decode(WeatherData.self, from: data)

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

The unit tests for the WeekViewModel struct are easy to write. The simplest unit test of this series is the one for the numberOfSections computed property. Remember that numberOfSections always returns 1.

WeekViewModelTests.swift

// MARK: - Tests for Number of Sections

func testNumberOfSections() {
    XCTAssertEqual(viewModel.numberOfSections, 1)
}

The unit test for numberOfDays is just as easy to implement. A single assertion is sufficient.

WeekViewModelTests.swift

// MARK: - Tests for Number of Days

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

Unit testing the viewModel(for:) method is slightly more complicated. We can take a few approaches. Remember that the viewModel(for:) method returns an object that conforms to the WeatherDayPresentable protocol. One approach is to ask the view model for the object that corresponds with a predefined index and assert that the day and date properties are equal to the values we expect based on the stub included in the unit testing bundle.

WeekViewModelTests.swift

// MARK: - Tests for View Model for Index

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

    XCTAssertEqual(weatherDayViewModel.day, "Saturday")
    XCTAssertEqual(weatherDayViewModel.date, "June 27")
}

These are the unit tests we need to write for the WeekViewModel struct. Press Command + U to run the test suite.

Running the Test Suite

Unit Testing the Weather Day View Model

You should now be able to write the unit tests for the WeatherDayViewModel struct. The unit tests are similar to those of the DayViewModel struct. The only difficulty is creating the view model. Give it a try to see if you can make it work. You can find the solution in the finished project of this episode.

We create a new XCTestCase subclass and name it WeatherDayViewModelTests.swift.

Creating WeatherDayViewModelTests.swift

We add an import statement for the Cloudy module and define a property with name viewModel. The viewModel property is an implicitly unwrapped optional.

WeatherDayViewModelTests.swift

import XCTest
@testable import Cloudy

class WeatherDayViewModelTests: XCTestCase {

    // MARK: - Properties

    var viewModel: WeatherDayViewModel!

    // MARK: - Set Up & Tear Down

    override func setUpWithError() throws {

    }

    override func tearDownWithError() throws {

    }

}

We create the view model in the setUpWithError() method. We load the stub from the unit testing bundle and convert it to a WeatherData object. We use the WeatherData object to create the WeatherDayViewModel object. Because we need a WeatherDayData object to create the WeatherDayViewModel object, we ask the WeatherData object for a WeatherDayData object. That is the only complexity of the unit tests for the WeatherDayViewModel struct.

WeatherDayViewModelTests.swift

override func setUpWithError() throws {
    // Load Stub
    let data = loadStub(name: "weather", extension: "json")

    // Create JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    // Decode JSON
    let weatherData = try decoder.decode(WeatherData.self, from: data)

    // Initialize View Model
    viewModel = WeatherDayViewModel(weatherDayData: weatherData.dailyData[5])
}

The unit tests should look familiar. They are similar to the ones we wrote for the DayViewModel struct.

WeatherDayViewModelTests.swift

// MARK: - Tests for Day

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

// MARK: - Tests for Date

func testDate() {
    XCTAssertEqual(viewModel.date, "June 27")
}

// MARK: - Tests for Temperature

func testTemperature_Fahrenheit() {
    let temperatureNotation: TemperatureNotation = .fahrenheit
    UserDefaults.standard.set(temperatureNotation.rawValue, forKey: "temperatureNotation")

    XCTAssertEqual(viewModel.temperature, "65 °F - 83 °F")
}

func testTemperature_Celsius() {
    let temperatureNotation: TemperatureNotation = .celsius
    UserDefaults.standard.set(temperatureNotation.rawValue, forKey: "temperatureNotation")

    XCTAssertEqual(viewModel.temperature, "18 °C - 28 °C")
}

// MARK: - Tests for Wind Speed

func testWindSpeed_Imperial() {
    let unitsNotation: UnitsNotation = .imperial
    UserDefaults.standard.set(unitsNotation.rawValue, forKey: "unitsNotation")

    XCTAssertEqual(viewModel.windSpeed, "6 MPH")
}

func testWindSpeed_Metric() {
    let unitsNotation: UnitsNotation = .metric
    UserDefaults.standard.set(unitsNotation.rawValue, forKey: "unitsNotation")

    XCTAssertEqual(viewModel.windSpeed, "10 KPH")
}

// MARK: - Tests for Image

func testImage() {
    let viewModelImage = viewModel.image
    let imageDataViewModel = viewModelImage!.pngData()!
    let imageDataReference = UIImage(named: "clear-day")!.pngData()!

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

In the tearDownWithError() method, we reset the state we set in the unit tests.

WeatherDayViewModelTests.swift

override func tearDownWithError() throws {
    // Reset User Defaults
    UserDefaults.standard.removeObject(forKey: "unitsNotation")
    UserDefaults.standard.removeObject(forKey: "temperatureNotation")
}

The view models we created in the previous episodes are now fully covered by unit tests. Run the test suite one more time to make sure the unit tests pass.

Running the Test Suite

What's Next?

In the second part of this series, we take the Model-View-ViewModel pattern to the next level by introducing bindings, an essential component of the Model-View-ViewModel pattern.