It’s time to unit test the AddLocationViewViewModel
class. Create a new unit test case class in the Test Cases group of the CloudyTests target and name it AddLocationViewViewModelTests.swift.
Remove the sample tests and add an import statement for RxSwift, RxTest, and RxBlocking. As I mentioned earlier, RxTest and RxBlocking make testing reactive code much easier. It’s one of the best features of the RxSwift project.
We also need to import the Cloudy module to make sure we have access to the AddLocationViewViewModel
class. Don’t forget to prefix the import statement with the testable attribute to make internal entities accessible from within the test target.
AddLocationViewViewModelTests.swift
Click the diamond in the gutter on the left to run the unit tests for the AddLocationViewViewModel
class. We don’t have any tests yet, but it ensures everything is set up correctly.
Before we can write any tests, we need to declare a few properties:
viewModel
of type AddLocationViewViewModel!
scheduler
of type SchedulerType!
query
of type Variable<String>!
AddLocationViewViewModelTests.swift
Notice that these properties are implicitly unwrapped optionals. Remember that safety isn’t a major concern when we write unit tests. If something goes wrong, it means we made a mistake in the test case, which we need to fix first.
The viewModel
property is the view model we’ll be testing. The scheduler
property is less important for our discussion. Schedulers provide a layer of abstraction for scheduling operations using RxSwift. That’s all you need to know about schedulers to follow along.
We use the query
property to mock user input. Because the view model doesn’t know where the input comes from, it’s easy to mock user input by emitting events with the help of the query
variable.
We set everything up in the setUp()
method of the test case. Remember that this method is executed every time a unit test is run. It ensures we start with a clean slate before a unit test is run.
In the setUp()
method, we create a Variable
of type String
and assign it to the query
property. The variable is initialized with an empty string.
AddLocationViewViewModelTests.swift
The next step is initializing the view model. But we have a problem. If we use the Geocoder
class we created earlier, we cannot stub the response of the geocoding request. Remember that we want to control the environment in which the test suite is run. If the application talks to a location service, we need the ability to control its response.
Fortunately, we already did the heavy lifting to make this very easy. All we need to do is create a mock location service. We start by declaring a private, nested class, MockLocationService
, which conforms to the LocationService
protocol.
AddLocationViewViewModelTests.swift
The only method we need to implement to conform to the LocationService
protocol is geocode(addressString:completionHandler:)
. If addressString
has a value and its value isn’t an empty string, we invoke the completion handler with an array containing one Location
instance. The second argument of the completion handler, an optional error, is nil
. Notice that we control what the location service returns. In this example, we return a Location
instance with a name of Brussels
and a fixed set of coordinates.
AddLocationViewViewModelTests.swift
If addressString
has no value or its value is equal to an empty string, we invoke the completion handler with an empty array. The second argument of the completion handler, an optional error, is nil
.
That’s it. In the setUp()
method, we can now instantiate an instance of the MockLocationService
class and pass it as an argument to the initializer of the AddLocationViewViewModel
class. The first argument of the initializer is the query
property as a driver.
AddLocationViewViewModelTests.swift
We also create a concurrent dispatch queue scheduler and assign it to the scheduler
property. Don’t worry about this if you’re not familiar with RxSwift.
AddLocationViewViewModelTests.swift
The first test we’re going to write tests the locations
driver of the view model. Now that we control what the location service returns, we can test the behavior of the view model. We name the test testLocations_HasLocations()
.
AddLocationViewViewModelTests.swift
We create an observable and store a reference to it in a constant named observable
.
AddLocationViewViewModelTests.swift
We emit a new event by setting the value of the query
property. This mimics the user entering a name of a city in the search bar of the add location view controller.
AddLocationViewViewModelTests.swift
We use toBlocking()
to make the test synchronous. The result is stored in the result
constant.
AddLocationViewViewModelTests.swift
The result is an array of Location
instances. We assert that result
isn’t equal to nil
and that it contains one element.
AddLocationViewViewModelTests.swift
We also include an assertion for the location stored in the array by making sure the name
property of the location is equal to Brussels.
AddLocationViewViewModelTests.swift
Notice that we make ample use of the exclamation mark. If anything blows up, it’s because of a failed test or an error we made. In other words, we made a mistake we need to fix. This is what the unit test looks like.
AddLocationViewViewModelTests.swift
We also need to unit test the behavior when the user enter’s an empty string. This is very similar. We name this test testLocations_NoLocations()
. I won’t go into the finer details of RxSwift, such as drivers replaying the last event when a subscriber is added. We expect an empty array of locations and that’s what we test.
AddLocationViewViewModelTests.swift
Let’s run the unit tests we have so far to make sure they pass. That’s looking good.
While I won’t be discussing every unit test of the AddLocationViewViewModel
class, I want to show you a few more. With the next unit test, we test the location(at:)
method of the AddLocationViewViewModel
class.
The test looks similar to the ones we wrote earlier. Instead of inspecting the value of locations
, we ask the view model for the location at index 0
. It shouldn’t be equal to nil
. We also assert that the name of the Location
instance is equal to Brussels
.
AddLocationViewViewModelTests.swift
We can create a similar test for an index that is out of bounds. In that case, the location(at:)
method returns nil
, which we can test for.
AddLocationViewViewModelTests.swift
You can find the other unit tests for the AddLocationViewViewModel
class in the completed project of this chapter. I hope it’s clear that writing unit tests for a view model with bindings isn’t that much more complicated.