Let Me Check My Scheduler

Dean Silfen
Peloton-Engineering

--

Writing Testable ViewModels With RxSwift

The Problem

Writing tests for RxSwift can be tricky. One core feature of RxSwift is that everything happens asynchronously by default. If you’ve hooked up all your observables and observers correctly, then your app will function as expected. Unfortunately, asynchronous code is hard to unit test because you have to deal in approximations and eventualities. However, if we organize our code correctly, testing our Rx code will become rather straightforward.

RxSwift in Practice with MVVM

Typically RxSwift is used in conjunction with MVVM, so let’s write a ViewModel to test with. When I refer to MVVM in iOS, I refer to an architecture where code is roughly divided by data transfer types (Models), views (UIView and UIViewController subclasses) and ViewModels. Views are mainly for drawing on the screen, Models have no logic and carry data from place to place. ViewModels are for getting the Models and transforming them in a way that makes the View simple. Although our architecture only has four letters and three components, that does not mean we are limited to only three kinds of objects. Each of these types may be supported by any number of types that vary in name and responsibility. Defining how an entire iOS app should design its architecture is out of scope for this blog post, though, so let’s move forward with our ViewModel.

Say we have an app for users to post pictures of their favorite musical instruments. In this app we want a flow for users to follow each other. When you follow a user, you get presented with a view that shows the following:

  1. How many followers you have
  2. Who you followed last

So, our app needs a way to create a relationship where a user follows another, then we gather that data and show it on screen.

Let’s start by thinking about networking. Say our backend has a nice RESTful interface for users. api/v1/followers/ will respond to GET requests with an array of our users. This endpoint will also allow clients to PUTs a user and will create new followers.

For our iOS client, we would need to make a network request, probably with URLSession. Instead of worrying about the specifics of how URLSession work, how about we make a protocol that will wrap our networking calls. This way we can implement a real networking type for production, but stub it out in our tests.

Beautiful! Now that we know what our networking layer will look like, let’s think about our ViewModel. We need a way to follow users. Let’s start there.

This is a great start! Now we have an object that can follow other users! Even better, since we specify our scheduler, we prevent our UI from hanging by moving our networking calls to a background queue. If you are unfamiliar with Schedulers, they are the type that decides where the work for an observable will be run.

Even though this is a good first step, when you look closely there is no real good way to unit test this object. This is a fire and forget call so, as a consumer, there is no way to know if the follow succeeded or not. On top of that, because we create the operation queue inside this object, even if we knew what to look for, there is no telling when it would actually happen. Let’s fix this.

We have two requirements left:

  1. How many followers you have
  2. Who you followed last

Since these requirements both use data from the same endpoint, We should be able to implement these as a transformation on an incoming observable from our followers() method. Lets wire our two observables together so that when you request a follower, if the response is successful, request an updated list of followers.

This object feels better suited for testing, we provide an input (a user), and we should be able to observe an output (a new count and the latest follower).

A first attempt at Unit Tests

In order to isolate what we are testing, we will inject a mock FollowerNetworkLayer that will return arbitrary test data. Since we created our FollowerNetworkLayer protocol, we can create a struct that takes static data and returns it for our test. We do not care to test our network layer, we can do that separately. Currently we only care to test that when we ask our ViewModel to follow a user, it properly returns the expected data. The fact that it needs a networking layer at all is tangential to our real purpose.

Here we use the .just(_:) static method on Observable to create our observables. .just(_:) will create a one item observable that will instantly return its value upon subscription. This is great for scenarios when you only need one value, but if you need multiple items in a stream then it begins to fall down. This isn’t an issue though, so let’s proceed to write our test case.

An imperfect and verbose test.

It would be perfectly acceptable to stop here. We have tests that are isolated from the most of the system. They will accurately reflect the health of your system. If this is acceptable, then go for it! However RxTest provides us with some tools that will make your tests even more predictable, and I think you’re going to love it.

Accurate and Readable tests with a TestScheduler

Instead of letting our async code dictate how we write our tests, let’s try and make our ViewModel more predictable. If we recall in our FollowUserViewModel, we create our own scheduler using an OperationQueue. Instead of letting the ViewModel do this itself, let’s pull that responsibility out to the consumer. This way, our ViewModel can be configured to make where our code is run predictable. Our modifications to this class should look something like this:

Dependency Injection to configure our ViewModel with a new Scheduler

Now, whoever instantiates a FollowUserViewModel may dictate where it does its work. For our ViewControllers or Views, this may be a background scheduler, but for our tests lets try and use something new.

Here is where TestScheduler will shine. A TestScheduler is a type of Scheduler that runs on its own test time instead of using real seconds.

According to the RxTest Docs

Test scheduler implements virtual time that is detached from local machine clock. This enables running the simulation as fast as possible and proving that all events have been handled.

So TestSchedulers are fast because they work on their own time, not real time. This means that with time related operators like debounce and delay, running them on a TestScheduler will not create any noticeable delay in tests.

TestSchedulers also allow users to create test observables where users can dictate the exact events and when those events are received by observers. Meaning you can specify the test time that will elapse between each event. This is vital for testing things like rate limiters and other time-based operators.

With TestScheduler, one may decide to create a hot or cold observable, to mimic observables of that type in production. Observables created this way can even specify an error or completed event.

TestSchedulers can also be used to create TestObservers which you can bind to observables. TestObservers allow you to inspect how many events they receive and what the values of the events are. You can do this because when you create a scheduler and hook up all your observables and observers, you call start(), which will block while the scheduler performs all events. Once the scheduler is finished (which is determined by a timeout value on initialization), you can see all that happened while it ran.

So with this new tool in mind, lets update our tests to inject a TestScheduler rather than using PublishSubjects & wait.

A test we can be proud of

We were able to eliminate a few lines of code for each observable because we no longer need to create a subscription on events that come into our PublishSubjects. Instead, we bind the observers from the FollowUserViewModel to two TestObservers. Then, once we run our scheduler, we check the events the test observers received.

Second, we were able to eliminate our wait(_:for:) call! Now that all the work is run in our contained environment, we don’t need to worry about out business code running after our test finishes!

Another happy side effect of this approach is that since our test runs in simulated test time, we will never hit a timeout associated with a wait(for:timeout:) call. This may not seem like a huge win, but if you have a lot of tests that all have timeouts, this can make a huge improvement on test times in failure cases.

Gotchas

One thing to be mindful of with TestSchedulers is that they do not behave well with infinite or long running subscriptions. I have only seen this be an issue around repeating timers. If a ViewModel has a timer to perform an action every so often, you must find a way to signal that the stream will end in some way. Otherwise the scheduler will timeout before the timer has completed.

Conclusion

When making any type that performs work on an observable, it is helpful to inject schedulers at initialization. They are a critical piece of the puzzle when it comes to how your object behaves. Being able to control it makes testing way less painful than the guesswork and timeouts of typical asynchronous code.

Referenced Articles

--

--