Search code examples
androidunit-testingrx-java2rx-binding

Unit testing to check that the accept was called when using RxBindings


AS 4.0.2
RxBindings 4.0.0

I have written a unit test for testing if the PublishRelay accept has been called that uses jakeWartons rxBindings

This is the snippet of code I am testing

private val checkStoresRelay: Relay<Unit> = PublishRelay.create<Unit>()

private fun onCheckStockTapped(view: View) {
    view.textViewCheckStock
        .clicks()
        .debounce(TIME_OUT, TimeUnit.MILLISECONDS)
        .subscribeBy(
            onNext = checkStoresRelay::accept,
            onError = { Timber.e(it, it.localizedMessage) }
        )
}

In the test class I am doing the following. The textViewCheckStock.performClick() will trigger and I check that the checkStoresRelay.accept(Unit) had been called.

But I am not sure about the availableDeliveryOptionsModel.checkStoresRelay.accept(Unit) As I don't think I should be calling this in the test. However, the test will fail if I remove this. Just wondering what would be the best way to test this?

@Test
fun `should check stores when checkStock is tapped`() {
    // Arrange
    val viewHolder = createViewHolder()
    availableDeliveryOptionsModel.bind(viewHolder)
    val actual = availableDeliveryOptionsModel.checkStoresRelay.test()

    // Act
    availableDeliveryOptionsModel.checkStoresRelay.accept(Unit)
    viewHolder.itemView.textViewCheckStock.performClick()

    // Assert
    actual.assertValue(Unit)
}

Many thanks for any suggestions,


Solution

  • You definitely shouldn't call relay.accept() directly within your test. accept should be called on your behalf in subscribeBy(). The problem is based on .debounce() operator.

    Debounce waits for a given amount of time and if there isn't subsequent emit (view click), it emits item downstream (into .subscribeBy()). This creates some delay and results in a test failure, because actual.assertValue(Unit) is called before click (emit) is delivered.

    You can solve this issue with RxSchedulerRule. Execution of debounce operator is done immediately on the current thread, so you'll basically handle item delivery to relay and run assertions after.

    I have simplified your example a little bit, but I hope the main point remains:

    class SO64328289 {
        val checkStoresRelay: Relay<Unit> = PublishRelay.create()
    
        fun onCheckStockTapped(view: View) {
            view.clicks()
                .debounce(1, TimeUnit.MILLISECONDS)
                .subscribeBy(
                    onNext = checkStoresRelay::accept,
                    onError = { Log.e("SO64328289", it.localizedMessage) }
                )
        }
    }
    
    @RunWith(RobolectricTestRunner::class)
    @Config(sdk = [28])
    class SO64328289Test {
    
        @get:Rule val schedulerRule = RxSchedulerRule()
    
        @Test
        fun `when button clicked then relay emits value`() {
            val tested = SO64328289()
            val view = View(ApplicationProvider.getApplicationContext())
    
            tested.onCheckStockTapped(view)
            val relayTestSubscription = tested.checkStoresRelay.test()
    
            view.performClick()
    
            relayTestSubscription.assertValue(Unit)
        }
    }
    

    I have used Robolectric, but it should also work with other testing frameworks. These are dependencies I have used in my test project if you want to try it for yourself:

    implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
    implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1'
    implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
    implementation "io.reactivex.rxjava2:rxjava:2.2.20"
    
    testImplementation 'com.github.Plastix.RxSchedulerRule:rx2:1.0.2' // requires jitpack.io maven
    testImplementation 'org.robolectric:robolectric:4.4'
    testImplementation 'junit:junit:4.13.1'
    testImplementation 'androidx.test:core:1.3.0'