Avoid racing with Rx Subjects & TestScheduler

Banner graphic RXjava and Android

RX is a great spell but at times certain things can make you wish you had more hair on your head if you miss out on some fundamentals. Here’s one of my ‘GOTCHA’ moments.

After spending about a day or so trying to figure out why a simple test kept failing, a man was frustrated.

@RunWith(MockitoJUnitRunner::class)
class HairPullingTest {
private var testScheduler = TestScheduler()
@Test
fun testVodoo() {
val subject:PublishSubject<Int> = PublishSubject.create()
val testObserver = subject
.subscribeOn(testScheduler)
.observeOn(testScheduler)
.test()
subject.onNext(1)
testScheduler.triggerActions()
testObserver.assertValueCount(1)
}
}
view raw HairPullingTest.kt hosted with ❤ by GitHub

Fast forward he also realised something weird

  • Substituting Subject with an Observable passes the test.
  • Replacing the TestScheduler with a Trampoline scheduler also passes the test.

So something unusual is up, so he heads over to StackOverflow and with some help from Dávid Karnok adds an assert statement.

Banner graphic test 1

WTF

So a man goes about adding logs to his test.

@RunWith(MockitoJUnitRunner::class)
class HairPullingTest {
private var testScheduler = TestScheduler()
@Test
fun testVodoo() {
val subject:PublishSubject<Int> = PublishSubject.create()
val testObserver = subject
.subscribeOn(testScheduler)
.observeOn(testScheduler)
.doOnSubscribe {
print("I've subscribed")
}.test()
print("Emitting Item")
subject.onNext(1)
testScheduler.triggerActions()
testObserver.assertValueCount(1)
}
}

and…

Questionable output

So what’s really happening here?

Everybody lies

Turns out the actual subscription(subscribeActual() method in PublishSubject) which adds the subscriber to Subject’s list of subscribers doesn’t happen until triggerActions is invoked on the TestScheduler. This results in a race condition where even though the subscriber seems subscribed, the actual subscription and emission happen concurrently. The emission is therefore ignored due to absence of any subscribers.

The solution is to invoke triggerActions immediately after subscribing and then again after emission.

@RunWith(MockitoJUnitRunner::class)
class DemoViewModelTest {
private var testScheduler = TestScheduler()
@Test
fun testVodoo() {
val subject: PublishSubject<Int> = PublishSubject.create()
val testObserver = subject
.subscribeOn(testScheduler)
.observeOn(testScheduler)
.test()
testScheduler.triggerActions()
subject.onNext(1)
testScheduler.triggerActions()
testObserver.assertValueCount(1)
}
}

Conclusion

Always add an extra triggerAction() with your TestScheduler right after subscribing your subjects to ensure the subsequent emission happen as intended.

Published 30 Nov 2018

I'm passionate about creating stuff around android. Be vary, observations are interlaced with humor.
Anvith Bhat on Twitter