For the last few posts we’ve been working on Streams – values that change over time. It’s not too difficult to get to grips with and we worked all the way up to integrating these inside a View Model. Another side of the coin though is testing our code. How do we test reactive code with RxSwift? We use a library called RxTest.

RxTest gives us access to a Test Scheduler. Schedulers are like a piece of work and usually we’ll run that on a specific thread such as in the previous posts where we moved background work onto the Main Thread using the Main Scheduler Instance. The only issue here is the same thing that sequences add to the Rx way of thinking: time itself. Streams can receive events at any time, so to simulate that we just fake the time they will appear, but we get specific about the order. What’s even better about this is say we have a networking call that has to take 20 seconds, well we can fake that 20 seconds and not have to wait that long with mocking.

A good way to think about View Models with Rx is by having an input that you need to supply them and an output they will produce. Naturally we want to test the output here but the input could be a search bar’s text value, this triggers a network call and the view model exposes an output value that we can test. It could be a list of results, or like in this case a String.

Now let’s look at how we’re going to test this visually:

As you can see. The View Model has an input and output, both of which are just there to mutate the data in some way. Data is ‘bound’ in, it gets changed and an observable is created as output at the end, just like a factory, except that we want to subscribe to that output at the end.

Let’s take a look at an example:

In your test target make sure you import RxSwift, RxTest as well as an @testable import of your project to access it’s view models and other classes like so:

1
2
3
4
5
import XCTest
import RxSwift
import RxTest
@testable import YourAppName
class YourAppName: XCTestCase {

I’m just using CocoaPods for this project but you’ll need to import both RxSwift and RxTest separately.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ViewModel {
    // This is our public property we'll send input to. Perhaps this could have been a seach bar's text or something a label's text value.
    public var input: BehaviorSubject<String> = BehaviorSubject<String>(value: "")
    // This is our output value that we will test
    lazy var output: BehaviorSubject<String> = BehaviorSubject<String>(value: "")
   
    let disposeBag = DisposeBag()
    // Simple init to automatically call our subscription automatically
    init() {
        setupBindings()
    }
   
    // A simple function to subscribe to changes
    private func setupBindings() {
        input.subscribe(onNext: { [weak self] value in
            self?.output.onNext("Changed \(value)")
        }).disposed(by: disposeBag)
    }
}

We’ve created a simple view model that has an input and output subject. We’ll bind to these later, but for now we create a subscription to the input, alter it and send out a next event to the output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CoRaXMoViViMTests: XCTestCase {

    func testExample() {
       
        let scheduler = TestScheduler(initialClock: 0)
        // This is what we will test and will the result of the output from the View Model
        let testString = scheduler.createObserver(String.self)
        // Create our view model
        let viewModel = ViewModel()
        // Always remember to dispose of subscriptions
        let disposeBag = DisposeBag()
       
        // Bind the output we'll test to the observable here
        viewModel.output.asDriver(onErrorJustReturn: "There was an error").drive(testString).disposed(by: disposeBag)

        // Now send the input to the View Model
        // Here we'll send "one" 10 'seconds' as a .next() event and repeat it two more times
        scheduler.createColdObservable([.next(10, "One"), .next(20, "Two"), .next(30, "Three")])
            .bind(to: viewModel.input)
            .disposed(by: disposeBag)
       
        scheduler.start()
    }
}

Let’s break this down. First we create a scheduler which we’ll run our observables through at specified ‘times’. These aren’t really seconds, but they can be thought of being like that and we’ll start with ‘0’.

Next we create a string that we’re going to test. You would create an observer for each element you wanted to test, but think of this as a ‘testable observable’ with a type declared upfront. In our case we’re going to test Strings.

Next we create our View Model instance and our dispose bag to handle getting rid of subscriptions.

Now the fun part – binding! This is the magic of Rx in action. We convert our observable to a Driver giving it a default value and ensuring it is run on the Main Scheduler (covered in other posts) we tell it to drive the test string we’re checking here.

Now we’ll send the data into our View Model, check the result is what we expect and start the scheduler. We do this by creating a cold observable. Cold observables just call all subscriptions if they have any whereas Hot observables fire regardless. We’ll bind it to our input and then check the result.

So how do we test our code? The same was we normally do with UI Tests: using XCTAssert(). Let’s add the following:

1
XCTAssertEqual(testString.events, [.next(0, "Changed "), .next(10, "Changed One"), .next(20, "Changed Two"), .next(30, "Changed Three")], "The observable sequence didn't look quite right. Failing the test.")

Here we are literally calling next events and passing in the time as the 1st parameter and the expected outcome. Remember the View Model is simply changing a String value so it is very easy to test in this case. We’ve got a handy little error message at the end.

So as you can see we get full control of the timing of events as well as their values and errors. Super-useful and only one more dependency to get all that testing value. There are other methods for going deeper with asynchronous events using RxBlocking but we’ll cover these soon!