One of my favorite features of RxSwift is its testing infrastructure, RxTest. And it’s an undersold one too, it’s not even mentioned on a Why RxSwift page. Let’s take a look at it on a real-world example - paging in a scroll view.
Requirements: Xcode 8.3, Swift 3.1, RxSwift 3.5
One of the main components responsible for paging in our app is PagingScrollViewModel which is defined like this:
As you can see it is initialized with a paging service and two observable sequences that represent user input (retryTap and scroll view didReachBottom). The outputs are two other observable sequences: pages and indicator.
One of the scenarios that I’d like to capture by unit tests is this:
When the user scrolls to the bottom of the scroll view automatically start loading the next page and display a footer view with an activity indicator. If the request for the next page fails, show a footer view with an error message and a “Retry” button.
It’s a relatively complex scenario which would normally seem hard to test. But it’s actually really easy using RxTest. Let’s first take a quick look at RxTest and then jump right into the test file.
The main component of RxTest is a TestScheduler class. It is a “virtual time scheduler” which allows you to control time. You can use it to:
Create test observables which emit specific events at specific points in virtual time. For example, you can mock “Retry” button tap like this:
Create test observers which you can subscribe to your actual observables to record all of the events emitted by them:
Let’s see how it all comes into practice in the actual test case.
That’s it! If you were to run this test case we see the recorded events printed out to the console:
This matches the expected events, the test is passed successfully.
The TestScheduler.record(_:) is a helper method borrowed from the tests in a RxSwift repo (there are a lot of other goodies too):
Because RxSwift is such a generic abstraction which provides a unified interface for all kinds of events (user input, async operations, data changing over time) we can also have such a simple yet powerful unified testing infrastructure.
There are other ways to write RxSwift tests one of which is called “marble tests”. The idea is to use “marble notation” to define expected events. There is an example of marble tests in a RxSwift repo.
The downside of this approach is that you have to make sure that everything happens on a TestScheduler. And if there is any “uncontrolled” asynchronous code which gets executed as part of your tests then you’ll still end up writing asynchronous tests.