Testing the DayViewViewModel
struct isn’t very different from testing the view models of the SettingsViewController
class. The only tricky aspect is instantiating a DayViewViewModel
instance in a unit test.
To instantiate a DayViewViewModel
instance, we need a model. Should we fetch weather data from the Dark Sky API during a test run? The answer is a resounding “no”. To guarantee that the unit tests for the DayViewViewModel
struct are fast and reliable, we need stubs.
The idea is simple. We fetch a response from the Dark Sky API, save it in the unit testing bundle, and load the response when we run the unit tests for the view model. Let me show you how this works.
I’ve already saved a response from the Dark Sky API to my desktop. This is nothing more than a plain text file with JSON data. Before we can use it in the test case, we add the file to the unit testing bundle. The JSON file is included with the source files of this chapter. Drag it in the Stubs group of the CloudyTests target.
Make sure that Copy items if needed is checked and that the file is only added to the CloudyTests target.
Because we’ll use the stub data in multiple test cases, we first create a helper method to load the stub data from the unit testing bundle. Create a new file in the Extensions group of the unit testing bundle and name it XCTestCase.swift.
Replace the import statement for Foundation with an import statement for XCTest and define an extensions for the XCTestCase
class.
XCTestCase.swift
Name the helper method loadStubFromBundle(withName:extension:)
.
XCTestCase.swift
The method accepts two parameters:
In loadStubFromBundle(withName:extension:)
, we fetch a reference to the unit testing bundle, ask it for the URL of the file we’re interested in, and use the URL to instantiate a Data
instance.
XCTestCase.swift
Notice that we force unwrap the url
optional and, Heaven forbid, use the try
keyword with an exclamation mark. This is something I only ever do when writing unit tests. You have to understand that we’re only interested in the results of the unit tests. If anything else goes wrong, we made a silly mistake, which we need to fix. In other words, I’m not interested in error handling or safety when writing and running unit tests. If something goes wrong, the unit tests fail anyway.
We can now create the test case for the DayViewViewModel
struct. Create a new test case and name the file DayViewViewModelTests.swift. We start by adding an import statement for the Cloudy module. Don’t forget to prefix the import statement with the testable
attribute.
DayViewViewModelTests.swift
To simplify the unit tests, we won’t be instantiating a view model in each of the unit tests. Instead, we create a view model, the view model we use for testing, in the setUp()
method. Let me show you how that works and what the benefits are.
We first define a property for the view model. This means every unit test will have access to a fully initialized view model, ready for testing.
DayViewViewModelTests.swift
Notice that the type of the property is an implicitly unwrapped optional. This is dangerous, but remember that we don’t care if the test suite crashes and burns. If that happens, it means that we made a mistake we need to fix. This is really important to understand. When we’re running the unit tests, we’re interested in the test results. We very often use shortcuts for convenience to improve the clarity and the readability of the unit tests. This’ll become clear in a moment.
In the setUp()
method, we invoke the loadStubFromBundle(withName:extension:)
helper method to load the contents of the stub we added earlier and we use the Data
object to instantiate a WeatherData
instance. The model is used to create the DayViewViewModel
instance we’re going to use in each of the unit tests.
DayViewViewModelTests.swift
The first unit test is as simple as unit tests get. We test the date
computed property of the DayViewViewModel
struct. We assert that the value of the date
computed property is equal to the value we expect.
DayViewViewModelTests.swift
We can keep the unit test this simple because we control the stub data. If we were to fetch a response from the Dark Sky API, we wouldn’t have a clue what would come back. It would be slow, asynchronous, and prone to all kinds of issues.
The second unit test we write is for the time
computed property of the DayViewViewModel
struct. Because the value of the time
computed property depends on the user’s preference, stored in the user defaults database, we have two unit tests to write.
DayViewViewModelTests.swift
The body of the first unit test looks very similar to some of the unit tests we wrote in the previous chapter. We set the time notation setting in the user defaults database and assert that the value of the time
computed property is equal to the value we expect. Let me repeat that we can only do this because we know the contents of the stub data and, as a result, the model the view model manages.
DayViewViewModelTests.swift
The second unit test for the time
computed property is very similar. Only the value we set in the user defaults database is different.
DayViewViewModelTests.swift
The remaining unit tests for the DayViewViewModel
struct follow the same pattern. Put the book aside and give them a try. I have to warn you, though, the unit test for the image
computed property is a bit trickier. But you can do this. You can find the remaining unit tests below.
DayViewViewModelTests.swift
The unit test for the image
computed property is slightly different. Comparing images isn’t straightforward. We first make an assertion that the value of the image
computed property isn’t nil
because it returns a UIImage?
.
DayViewViewModelTests.swift
We then convert the image to a Data
object and compare it to a reference image, loaded from the application bundle. You can go as far as you like. For example, I’ve also added assertions for the dimensions of the image. This isn’t critical for this application, but it shows you what’s possible.
DayViewViewModelTests.swift
Before we run the test suite, we need to tie up some loose ends. In the tearDown()
method, we reset the state we set in the unit tests.
DayViewViewModelTests.swift
Press Command + U to run the test suite to make sure the unit tests for the DayViewViewModel
struct pass.
In the next chapter, we unit test the view models for the WeekViewController
class.