简体   繁体   中英

how to unit test a function that returns livedata

In my viewModel, I have a function that returns liveData. This function is directly called in the fragment and hence it is observed directly there. I am not able to get how can I test this function since liveData emitted by the function is not getting observed in case of tests and hence it will not return the value.

This is my function, I want to write test for:

    fun saveRating(rating: Float, eventName: String): LiveData<Response<SaveRatingData?>?> {
        val request = RatingRequest(rating.toDouble(), eventName, false)

        return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(repository.saveRatings(request))
        }

    }

and this is how I am calling it in a fragment:

   viewModel.saveRating(rating, npsEventData?.eventName ?: "").observe(this, Observer {
      // on getting data
   })

Thanks in advance!

You need to have a testCoroutineDispatcher or testCoroutineScope to be able to set scope of your viewModel to scope of testing.

class TestCoroutineRule : TestRule {

    private val testCoroutineDispatcher = TestCoroutineDispatcher()

    val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description?) = object : Statement() {

        @Throws(Throwable::class)
        override fun evaluate() {

            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain()
            try {
                testCoroutineScope.cleanupTestCoroutines()
            } catch (exception: Exception) {
                exception.printStackTrace()
            }
        }
    }

    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
        testCoroutineScope.runBlockingTest { block() }

}

Try-catch block is not mentioned in any official kotlin or Android documents but testing exceptions cause exceptions instead of passing the test as i asked in this here .

And another thing i experienced with testCoroutineDispatcher as dispatcher is not enough for some test to pass, you need to inject coroutineScope instead of dispatcher to viewModel.

For instance

fun throwExceptionInAScope(coroutineContext: CoroutineContext) {


    viewModelScope.launch(coroutineContext) {

        delay(2000)
        throw RuntimeException("Exception Occurred")
    }
}

You have a function like this that throws exception and you pass testCoroutineContext to this test it fails.

@Test(expected = RuntimeException::class)
fun `Test function that throws exception`() =
    testCoroutineDispatcher.runBlockingTest {

        // 🔥 Using testCoroutineDispatcher causes this test to FAIL
        viewModel.throwExceptionInAScope(testCoroutineDispatcher.coroutineContext)

        // 🔥 This one passes since we use context of current coroutineScope
        viewModel.throwExceptionInAScope(this.coroutineContext)

    }

It passes if you use class MyViewModel(private val coroutineScope: CoroutineScope)

Now, let's get to how to test liveData with asynchronous tasks. I use this class, Google's LiveDataTestUtil class, for synching liveData and

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

as rule

fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    afterObserve.invoke()

    // Don't wait indefinitely if the LiveData is not set.
    if (!latch.await(time, timeUnit)) {
        this.removeObserver(observer)
        throw TimeoutException("LiveData value was never set.")
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

/**
 * Observes a [LiveData] until the `block` is done executing.
 */
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
    val observer = Observer<T> { }
    try {
        observeForever(observer)
        block()
    } finally {
        removeObserver(observer)
    }
}

Now, you can test it same as you test synchronous code

@Test
fun `Given repo saves response, it should return the correct one` = testCoroutineScope.runBlockingTest {

        // GIVEN
        val repository = mockk<<Repository>()
        val actual = Response(...)
        coEvery { repository.saveRatings } returns actual

        // WHEN
        val expected = viewModel.saveResponse()

        // THEN
        Truth.assertThat(actual).isEqualTo(expected)

    }

I used mockK, which works well with suspending mocking.

Also you don't need to use Dispatchers.IO if you have retrofit or Room function calls, they use their own thread with suspend modifier if you are not doing other task than retrofit or room actions.

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