Search code examples
androidunit-testingkotlinmockkkotest

How to write unit test for the method returns LiveData with Kotest and Mockk library


I am using MVVM to architecutre my android app, my repository has a method which query data from Room Database and returns a LiveData, the signure of my method is:

fun getFolder(id: Long): LiveData<Folder?>

I want to write a unit test for this method with the following code:

import androidx.lifecycle.MutableLiveData
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import my.package.Folder
import my.package.FolderRepository
import java.util.*

class FolderRepositoryTest: FunSpec({

    val repository = mockk<FolderRepository>()
    val folder = Folder(
      // folder field init code    
    )
    val folderLiveData = MutableLiveData(folder)


    test("FolderRepository getFolder works as expected") {
        val id = folder.id.toLong()
        every {  repository.getFolder(any()) } returns folderLiveData
        repository.getFolder(id)
        verify {
            repository.getFolder(id)
        } shouldBe folderLiveData
    }
    
})

But the test failed wit the following failure message.

io.kotest.assertions.AssertionFailedError: expected:androidx.lifecycle.MutableLiveData@1afc7182 but was:<kotlin.Unit>

expected:<androidx.lifecycle.MutableLiveData@1afc7182> but was:<kotlin.Unit>
Expected :androidx.lifecycle.MutableLiveData@1afc7182
Actual   :kotlin.Unit

Can anybody help me point out where I am wrong and how to write unit test cases with kotest library and Mockk library.


Solution

  • Finally, I got the answer at Google Sample! First create the extension methods for LiveData.

    fun <T> LiveData<T>.getOrAwaitValue(
            time: Long = 2,
            timeUnit: TimeUnit = TimeUnit.SECONDS,
            afterObserver: () -> Unit = {}
    ): T {
        var data: T? = null
        val latch = CountDownLatch(1)
        val observer = object: Observer<T> {
            override fun onChanged(t: T) {
                data = t
                latch.countDown()
                [email protected](this)
            }
        }
        this.observeForever(observer)
        afterObserver.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
    }
    
    fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
        val observer = Observer<T> { }
        try {
            observeForever(observer)
            block()
        } finally {
            removeObserver(observer)
        }
    }
    

    Second, create a test config for your app

    import androidx.arch.core.executor.ArchTaskExecutor
    import androidx.arch.core.executor.TaskExecutor
    import io.kotest.core.config.AbstractProjectConfig
    import kotlinx.coroutines.test.TestCoroutineDispatcher
    // This is necessary, otherwise you will got Looper not mocked failed.
    object TestConfig: AbstractProjectConfig() {
        private val testDispatcher = TestCoroutineDispatcher()
    
        override suspend fun beforeProject() {
            super.beforeProject()
            setupLiveData()
        }
    
        override suspend fun afterProject() {
            super.afterProject()
            resetLiveData()
        }
    
        private fun setupLiveData() {
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun postToMainThread(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun isMainThread(): Boolean {
                    return true
                }
            })
        }
    
        private fun resetLiveData() {
            ArchTaskExecutor.getInstance().setDelegate(null)
        }
    }
    

    And at last, I changed my code a bit and the test passed, here is the code

    test("FolderRepository should works as expected")
        .config(testCoroutineDispatcher = true) {
                val id = folder.id.toLong()
                val result = MutableLiveData(folder)
                every { folderRepository.getFolder(any()) } returns result
                val f = folderRepository.getFolder(id).getOrAwaitValue()
                f shouldBe result.value
                verify { folderRepository.getFolder(withArg {
                    assertTrue(it == id)
                }) }
                verify { folderRepository.getFolder(id) }
            }
    

    Maybe there is better solution, hope you can post an answer and let's improve our code!!