Skip to content

Instantly share code, notes, and snippets.

@OliverCulleyDeLange
Last active August 2, 2023 14:29
Show Gist options
  • Save OliverCulleyDeLange/84aa4d2b299b2dfff3746bfdf346cd3e to your computer and use it in GitHub Desktop.
Save OliverCulleyDeLange/84aa4d2b299b2dfff3746bfdf346cd3e to your computer and use it in GitHub Desktop.
Test LiveData with KoTest and Mockk

Testing ViewModel / LiveData with KoTest (previously Kotlintest) and Mockk

I went round a few houses trying to figure out how best to test ViewModels containing LiveData using Kotlin based testing tools. My main mistakes were trying to use JUnit 5 annotations with KoTest test classes and expecting them to work. The @ExtendWith() annotation is from the org.junit.jupiter.api.extension package. Jupiter is the module used for writing tests, whereas KoTest runs on the JUnit 5 Platform. Very different thing apparently

I came accross this Mockk issue which pointed me towards this write up on how to test live data with JUnit 5. This gave me the code to delegate the live data executor to a custom one which simply executes immedietely.

I've not tested this extensively, but it seems to get around the usual hurdle of java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. in a re-usable way, with mocks thrown in for good measure.

...
android {
...
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
}
dependencies {
...
def koTestVersion = "4.0.2"
testImplementation "io.kotest:kotest-runner-junit5-jvm:$koTestVersion"
testImplementation "io.kotest:kotest-assertions-core-jvm:$koTestVersion"
testImplementation "io.mockk:mockk:1.9.3"
testImplementation "androidx.arch.core:core-testing:2.1.0"
}
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MyViewModel(private val repo: MyRepository) : ViewModel() {
private val _data = MutableLiveData<String>().apply { value = "Loading..." }
val data: LiveData<String>
get() = _data
fun doThing() {
_data.postValue(repo.getStuff())
}
}
class MyRepository {
fun getStuff(): String {
return "A thing"
}
}
package uk.co.oliverdelange.android-stuff.viewmodel
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import io.kotest.core.listeners.TestListener
import io.kotest.core.spec.Spec
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
class ViewModelSpec : StringSpec() {
lateinit var vm: MyViewModel
lateinit var mockRepo: MyRepository
init {
listener(InstantExecutorListener())
beforeTest {
mockRepo = mockk()
vm = MyViewModel(mockRepo)
}
"test live data updated when doThing()" {
vm.data.value shouldBe "Loading..."
val expected = "Something specific"
every { mockRepo.getStuff() } returns expected
vm.doThing()
vm.data.value shouldBe expected
}
}
}
// Based off https://jeroenmols.com/blog/2019/01/17/livedatajunit5/
class InstantExecutorListener : TestListener {
override suspend fun afterSpec(spec: Spec) {
super.afterSpec(spec)
print("Removing Instant Executor Delegate")
ArchTaskExecutor.getInstance().setDelegate(null)
}
override suspend fun beforeSpec(spec: Spec) {
super.beforeSpec(spec)
print("Delegating to Instant Executor")
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment