16 A Few More Unit Tests

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

Unit Testing the Week View View Model

Create a new XCTestCase subclass and name the file WeekViewViewModelTests.swift.

Creating WeekViewViewModelTests.swift
Creating WeekViewViewModelTests.swift

Add an import statement for the Cloudy module and define a property for the view model like we did in the previous chapter. The approach we take is identical to the approach we took in the previous chapter. The type of the property is an implicitly unwrapped optional, WeekViewViewModel!.

WeekViewViewModelTests.swift

 1 import XCTest
 2 @testable import Cloudy
 3 
 4 class WeekViewViewModelTests: XCTestCase {
 5 
 6     // MARK: - Properties
 7 
 8     var viewModel: WeekViewViewModel!
 9 
10     // MARK: - Set Up & Tear Down
11 
12     override func setUp() {
13         super.setUp()
14     }
15 
16     override func tearDown() {
17         super.tearDown()
18     }
19 
20 }

In the setUp() method, we load the same stub from the unit testing bundle, instantiate a WeatherData instance with it, and use the value of the dailyData property to instantiate the view model. Remember that the dailyData property is of type [WeatherDayData].

WeekViewViewModelTests.swift

 1 override func setUp() {
 2     super.setUp()
 3 
 4     // Load Stub
 5     let data = loadStubFromBundle(withName: "darksky", extension: "j\
 6 son")
 7     let weatherData: WeatherData = try! JSONDecoder.decode(data: dat\
 8 a)
 9 
10     // Initialize View Model
11     viewModel = WeekViewViewModel(weatherData: weatherData.dailyData)
12 }

The unit tests for the WeekViewViewModel struct are very easy to write. The simplest unit test of this book is the one for the numberOfSections computed property since it always returns the value 1.

WeekViewViewModelTests.swift

1 // MARK: - Tests for Number of Sections
2 
3 func testNumberOfSections() {
4     XCTAssertEqual(viewModel.numberOfSections, 1)
5 }

The unit test for numberOfDays is also easy to write. One assertion is all we need.

WeekViewViewModelTests.swift

1 // MARK: - Tests for Number of Days
2 
3 func testNumberOfDays() {
4     XCTAssertEqual(viewModel.numberOfDays, 8)
5 }

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

WeekViewViewModelTests.swift

1 // MARK: - Tests for View Model for Index
2 
3 func testViewModelForIndex() {
4     let weatherDayViewViewModel = viewModel.viewModel(for: 5)
5 
6     XCTAssertEqual(weatherDayViewViewModel.day, "Saturday")
7     XCTAssertEqual(weatherDayViewViewModel.date, "July 15")
8 }

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

Running the Test Suite
Running the Test Suite

Unit Testing the Weather Day View View Model

You should now be able to write the unit tests for the WeatherDayViewViewModel struct. The unit tests are very similar to those of the DayViewViewModel struct. The only difficulty is instantiating the view model. Give it a try to see if you can make it work. You can find the solution below.

We create a new file and name it WeatherDayViewViewModelTests.swift.

Creating WeatherDayViewViewModelTests.swift
Creating WeatherDayViewViewModelTests.swift

We add an import statement for the Cloudy module and define a property for the view model of type WeatherDayViewViewModel!, an implicitly unwrapped optional.

WeatherDayViewViewModelTests.swift

 1 import XCTest
 2 @testable import Cloudy
 3 
 4 class WeatherDayViewViewModelTests: XCTestCase {
 5 
 6     // MARK: - Properties
 7 
 8     var viewModel: WeatherDayViewViewModel!
 9 
10     // MARK: - Set Up & Tear Down
11 
12     override func setUp() {
13         super.setUp()
14     }
15 
16     override func tearDown() {
17         super.tearDown()
18     }
19 
20 }

We instantiate the view model in the setUp() method. We load the stub from the unit testing bundle, create a WeatherData instance with it, and use the WeatherData instance to initialize the view model. Because we need a WeatherDayData instance to initialize the view model, we ask the WeatherData instance for one. This is the only complexity of the unit tests for the WeatherDayViewViewModel struct.

WeatherDayViewViewModelTests.swift

 1 override func setUp() {
 2     super.setUp()
 3 
 4     // Load Stub
 5     let data = loadStubFromBundle(withName: "darksky", extension: "j\
 6 son")
 7     let weatherData: WeatherData = try! JSONDecoder.decode(data: dat\
 8 a)
 9 
10     // Initialize View Model
11     viewModel = WeatherDayViewViewModel(weatherDayData: weatherData.\
12 dailyData[5])
13 }

The unit tests should look familiar. They’re similar to the ones we wrote for the DayViewViewModel struct.

WeatherDayViewViewModelTests.swift

 1 // MARK: - Tests for Day
 2 
 3 func testDay() {
 4     XCTAssertEqual(viewModel.day, "Saturday")
 5 }
 6 
 7 // MARK: - Tests for Date
 8 
 9 func testDate() {
10     XCTAssertEqual(viewModel.date, "July 15")
11 }
12 
13 // MARK: - Tests for Temperature
14 
15 func testTemperature_Fahrenheit() {
16     let temperatureNotation: TemperatureNotation = .fahrenheit
17     UserDefaults.standard.set(temperatureNotation.rawValue, forKey: \
18 UserDefaultsKeys.temperatureNotation)
19 
20     XCTAssertEqual(viewModel.temperature, "37 째F - 47 째F")
21 }
22 
23 func testTemperature_Celsius() {
24     let temperatureNotation: TemperatureNotation = .celsius
25     UserDefaults.standard.set(temperatureNotation.rawValue, forKey: \
26 UserDefaultsKeys.temperatureNotation)
27 
28     XCTAssertEqual(viewModel.temperature, "3 째C - 8 째C")
29 }
30 
31 // MARK: - Tests for Wind Speed
32 
33 func testWindSpeed_Imperial() {
34     let unitsNotation: UnitsNotation = .imperial
35     UserDefaults.standard.set(unitsNotation.rawValue, forKey: UserDe\
36 faultsKeys.unitsNotation)
37 
38     XCTAssertEqual(viewModel.windSpeed, "1 MPH")
39 }
40 
41 func testWindSpeed_Metric() {
42     let unitsNotation: UnitsNotation = .metric
43     UserDefaults.standard.set(unitsNotation.rawValue, forKey: UserDe\
44 faultsKeys.unitsNotation)
45 
46     XCTAssertEqual(viewModel.windSpeed, "2 KPH")
47 }
48 
49 // MARK: - Tests for Image
50 
51 func testImage() {
52     let viewModelImage = viewModel.image
53     let imageDataViewModel = UIImagePNGRepresentation(viewModelImage\
54 !)!
55     let imageDataReference = UIImagePNGRepresentation(UIImage(named:\
56  "cloudy")!)!
57 
58     XCTAssertNotNil(viewModelImage)
59     XCTAssertEqual(viewModelImage!.size.width, 236.0)
60     XCTAssertEqual(viewModelImage!.size.height, 172.0)
61     XCTAssertEqual(imageDataViewModel, imageDataReference)
62 }

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

WeatherDayViewViewModelTests.swift

1 override func tearDown() {
2     super.tearDown()
3 
4     // Reset User Defaults
5     UserDefaults.standard.removeObject(forKey: UserDefaultsKeys.unit\
6 sNotation)
7     UserDefaults.standard.removeObject(forKey: UserDefaultsKeys.temp\
8 eratureNotation)
9 }

Well done. We’ve now fully covered the view models with unit tests. Run the test suite one more time to make sure all the unit tests pass.

Running the Test Suite
Running the Test Suite

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