How to Unit Test Dispatching ReSwift Actions from RxSwift Observables

null

Say you are like me and work with ReSwift for unidirectional data flow goodness and employ RxSwift for your reactive cravings. You want to dispatch a ReSwift.Action when an RxSwift.Observable signal produces a new value. How do you write tests for that wiring?

In code, it could look like this:

someObservable
    .map { SomeAction($0) }
    .subscribe(onNext: store.dispatch($0))
    .addDisposableTo(disposeBag)

Testing that code requires a bit more understanding of both libraries than their respective documentation reveals.

Here’s an example:

import ReSwift

struct AppState {
    var count = 0
}

typealias DefaultStore = Store<AppState>

struct ChangingCount: ReSwift.Action {
    let value: Int
    init(value: Int) {
        self.value = value
    }
}

Imagine a reducer that takes incoming ChangingCount.value and replaces AppState.value with that. Simple.

Now you want to dispatch a ChangingCount action when some signal produces a new value. You need to create an observer for the signal and store it somewhere. So a friendly helper class comes to mind:

import RxSwift

class Dispatcher {
    let store: DefaultStore
    let disposeBag = DisposeBag()
    
    init(store: DefaultStore) {
        self.store = store
    }
    
    func wireSignalToAction(signal: Observable<Int>) {
        signal.map(ChangingCount.init(value:))
            .subscribe(onNext: { [weak self] in
                self?.store.dispatch($0) })
            .addDisposableTo(disposeBag)
    }
}

And that’s it. Now how can you make sure it does what it should do? What do unit tests look like?

Taking a look at RxSwift’s TestObserver and TestScheduler, you can record incoming events yourself and perform checks. To record events, you hijack a ReSwift.Store’s dispatchFunction with a closure that captures the incoming action. Using a TestScheduler, you can time production of events in the signal and keep track of the time the action comes in:

import RxSwift
import RxTest

class DispatchingTests: XCTestCase {
    /// Replacement for your real reducer because you won't need it.
    struct NullReducer: ReSwift.Reducer {
        func handleAction(action: Action, state: AppState?) -> AppState {
            return AppState(value: -1)
        }
    }
    
    func testDispatchesValueChanges() {
        
        // Set up a signal. Subscription per default starts at time=200
        let scheduler = TestScheduler(initialClock: 0)
        let signal = scheduler.createHotObservable([
            next(300, 98),
            next(600, 12)
            ])
        
        // Set up the store and record matching actions.
        let storeDouble = Store(reducer: NullReducer(), state: nil)
        var actions = [Recorded<Event<ChangingCount>>]()
        storeDouble.dispatchFunction = {
            guard let action = $0 as? ChangingCount else { return () }
            // `return f()` where `f() -> Void` is just like `return ()`.
            // We need to return something to satisy the `-> Any`.
            return actions.append(Recorded(time: scheduler.clock, value: .next(action)))
        }

        // Configure the object under test and start emitting
        // values in virtual time.
        let dispatcher = Dispatcher(store: storeDouble)
        dispatcher.wireSignalToAction(signal)
        scheduler.start()

        XCTAssertEqual(actions, [
            next(300, ChangingCount(value: 98)),
            next(600, ChangingCount(value: 12))
            ])
    }
}