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.