简体   繁体   中英

How to test RxSwift Observable.interval progress

I have a view model who's behaviour is controlled by an Observable.interval . In essence, it updates a timer label on every next and after a certain period, updates another value.

A trimmed example:

class WorkoutViewModel {
    private var _oneSecondTimer: Observable<Int> {
        return Observable<Int>.interval(1, scheduler: MainScheduler.instance)
    }
    private let _exerciseRemainingTime: Variable<Int> = Variable(20)

    func setupBehaviour() {
        _oneSecondTimer.subscribeNext() { [unowned self] _ in
            self._exerciseRemainingTime.value -= 1
            if self._exerciseRemainingTime.value == 0 {
                self.progressToNextExercise()
            }
        }
    }
}

I would like to have a test for this, to observe the time of event and value of _exerciseRemainingTime .

Is there a way how to use the TestScheduler to simulate virtual time in which the _oneSecondTimer would tick?

Yea, check out this simple test that has the behavior you're looking for: https://github.com/ReactiveX/RxSwift/blob/b3f4bf1/Tests/RxSwiftTests/Tests/Observable+TimeTest.swift#L496

To swap TestScheduler for MainScheduler , I would suggest you inject it as a dependency.

Also, to check on the value of _exerciseRemainingTime , you would need to remove private . I wouldn't suggest testing the internals of your class, however. Removing private is a sign that you are. Instead, if you were to inject an object whose responsibility is to do progressToNextExercise , then you could test that it received a call to progress to the next exercise. You would just pass in a test version of that object during tests, like you do with TestScheduler for the scheduler. That would remove the need to make _exerciseRemainingTime public to test it, or even know about it.

However, ignoring the visibility of _exerciseRemainingTime for the sake of this question's main purpose, here's what I mean about the scheduler:

WorkoutViewModel.swift

class WorkoutViewModel {
    private var _oneSecondTimer: Observable<Int> {
        return Observable<Int>.interval(1, scheduler: scheduler)
    }

    // not `private` anymore.  also, a computed property
    var _exerciseRemainingTime: Observable<Int> {
        return self._oneSecondTimer.map { i in
            20 - i
        }
    }

    // injected via `init`
    private let scheduler: SchedulerType

    init(scheduler: SchedulerType) {
        self.scheduler = scheduler
    }
}

WorkoutViewModelTest.swift

func testExerciseRemainingTime() {
    let scheduler = TestScheduler(initialClock: 0)

    let res = scheduler.start(0, subscribed: 0, disposed: 23) {
        WorkoutViewModel(scheduler: scheduler)._exerciseRemainingTime
    }

    let correct = [
        next(1, 20), // output .Next(20) at 1 second mark
        next(2, 19), // output .Next(19) at 2 second mark
        next(3, 18),
        next(4, 17),
        next(5, 16),
        next(6, 15),
        next(7, 14),
        next(8, 13),
        next(9, 12),
        next(10, 11),
        next(11, 10),
        next(12, 9),
        next(13, 8),
        next(14, 7),
        next(15, 6),
        next(16, 5),
        next(17, 4),
        next(18, 3),
        next(19, 2),
        next(20, 1),
        next(21, 0),
        next(22, -1),
    ]

    XCTAssertEqual(res.events, correct)
}

A couple notes to consider:

To let the test scheduler subscribe and dispose, I removed the subscribeNext from the view model. I think this improves it anyway, as you should be subscribing with the view controller and only using the view model to provide you with Observable . This obviates the need for the view model to have a dispose bag and manage the lifecycle of Disposable s.

You should really consider exposing something less "internal" than _exerciseRemainingTime . Maybe something like currentExercise: Observable<ExerciseEnum> , which is internally based on _exerciseRemainingTime . That way, your view controller can subscribe and do the simple view controller-related job of segueing into the next exercise.

Also, to simplify the test, you can inject the 20 variable into the view model so that in tests you can supply something smaller like 3 and then correct will only need to be a few elements long.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM