For example, I created an example code that needs to be tested
class MyViewModel() : BaseViewModel() {
private val valueSubject = BehaviorSubject.create<String>()
private val viewState = MutableLiveData<State>()
fun getViewState(): LiveData<State> = viewState
init {
disposables.add(valueSubject
.debounce(150L, TimeUnit.MILLISECONDS) //problem
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewState.value = State(it)
})
}
fun valueChanged(value: String) {
viewState.value = State(value + " processing")
valueSubject.onNext(value)
}
}
For example, you need to write a text that, with valueChanged ("12"), would catch 2 viewState changes, the first change is viewState.value = State (value + "processing"), and the second is already in the valueSubject subscription.
The problem as I understood it here in debounce(150L, TimeUnit.MILLISECONDS), there are such solution examples, but is it possible somehow without throwing the sheduller?
Basic part of the test
#RunWith(MockitoJUnitRunner::class)
class Test {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.onComputationScheduler(Schedulers.trampoline())
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
}
}
As you've already found out by yourself, moving the scheduler's clock forward is the way to go when dealing with all sorts of "delay" operators (such as delay, interval, timer and in your case debounce).
To help you out in the setup process, you could use a custom rule class and directly trigger your scheduler's events.
class RxTestSchedulerRule : Rule {
lateinit var testScheduler: TestScheduler
override fun onBefore() {
testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
RxJavaPlugins.setComputationSchedulerHandler { testScheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { testScheduler }
RxJavaPlugins.setSingleSchedulerHandler { testScheduler }
RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { testScheduler }
}
override fun onAfter() {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
fun CompositeRules.testScheduler() = getRule<RxTestSchedulerRule>().testScheduler
fun CompositeRules.advanceTimeBy(duration: Long, unit: TimeUnit) = getRule<RxTestSchedulerRule>().testScheduler.advanceTimeBy(duration, unit)
fun CompositeRules.advanceTimeTo(duration: Long, unit: TimeUnit) = getRule<RxTestSchedulerRule>().testScheduler.advanceTimeTo(duration, unit)
fun CompositeRules.triggerActions() = getRule<RxTestSchedulerRule>().testScheduler.triggerActions()
Then in your test class can be simplified with this
#RunWith(MockitoJUnitRunner::class)
class Test {
#get:Rule
var rule: TestRule = RxTestSchedulerRule()
#Test
fun `test view state correctly handled`() {
// Setup your stream
advanceTimeBy(150L, TimeUnit.MILLISECONDS)
// Assert obs has emitted view state
}
}
```
Related
I have a SharedFlow. When the ViewModel is created, I change the value to Val1. After that, I use the viewModelScope to make some fake delay of 3 seconds and then change the value to Val2.
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
init {
x.tryEmit("Val1")
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
Question
How do I test the initial value is Val1?
How do I test if the value has changed to Val2 after delay?
I found the solution:
It's as simple as setting the Main dispatcher to TestCoroutineDispatcher.
#ExperimentalCoroutinesApi
class CoroutineMainExtension : BeforeEachCallback, AfterEachCallback {
val dispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
}
and use it like this:
#ExtendWith(CoroutineMainExtension::class)
To test it, you need a way to inject your testing context. It is typically done by setting it as Dispatchers.Main.
Then the easy path is to use MutableStateFlow instead of MutableSharedFlow. Here is an example:
class MyViewModel : ViewModel() {
val x = MutableStateFlow("Val1")
init {
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
// then
assertEquals("Val1", myViewModel.x.value)
// when
testDispatcher.advanceTimeBy(3000)
// then
assertEquals("Val2", myViewModel.x.value)
}
}
If you want to test MutableSharedFlow, you should better move your logic from the constructor to some function, like onCreate. Then you should collect and observe how your values change. Here is an example (we could make a better one with some testing library like Turbine):
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
fun onCreate() {
viewModelScope.launch {
x.emit("Val1")
delay(3000)
x.emit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
var xChangeHistory = mapOf<Long, String>()
myViewModel.viewModelScope.launch {
myViewModel.x.collect {
xChangeHistory += testDispatcher.currentTime to it
}
}
// then
myViewModel.onCreate()
testDispatcher.advanceUntilIdle()
// then
assertEquals(mapOf(0L to "Val1", 3000L to "Val2"), xChangeHistory)
}
}
I created an instrumental test for a use case which has two coroutines, before fetching data and after. I run a test on the usecase but the test passes before the suspend function is done.
Usecase:
class FetchAllUseCase : Observable<FetchAllUseCase.Listener>() {
interface Listener {
fun onFetched(obj: List<Obj>)
}
fun fetchObjs() {
GlobalScope.launch(Dispatchers.IO) {
withContext(NonCancellable) {
val objs: List<Obj> = getObjs()
notify(objs)
}
}
}
private suspend fun notify(objs: List<Objs>) {
withContext(Dispatchers.Main) {
for(listener in listeners){
listener.onFetched(objs)
}
}
}
private fun getObjss(): List<Obj> {
/// fetch objects functionality
}
Test:
class FetchAllUseCase Test: FetchAllUseCase .Listener {
private lateinit var fetchAllUseCase: FetchAllUseCase
#Before
fun setUp() {
fetchAllUseCase = FetchAllUseCase()
fetchAllUseCase.registerListener(this)
}
#After
fun tearDown() {
fetchAllUseCase.unregisterListener(this)
}
#ExperimentalCoroutinesApi
#Test
fun fetchObjss()= runBlocking{
fetchAllUseCase.fetchObjs()
}
override fun onFetched(objs: List<Objs>) {
fail() //does not fail the test
}
}
Test already passes before onFetched() is called.
fail() or any other assertEquals that should fail the test does not affect the test result.
If I run the test on the MainActivity , adding:
#Rule
#JvmField
val mMainActivityTestRule = ActivityTestRule(MainActivity::class.java)
I will get a message "Process crashed" , but the test already passed.
If I remove both coroutines, the test works as expected, i.e fails.
I tried runBlocking, runBlockingTest, TestCoroutineDispatcher
None solved the issue so far
Add Dispatchers as Dependencies
class FetchAllUseCase(
private val mainDispatcher : CoroutineDispatcher = Dispatchers.Main,
private val ioDispatcher : CoroutineDispatcher = Dispatchers.IO,
) : Observable<FetchAllUseCase.Listener>() {
interface Listener {
fun onFetched(obj: List<Obj>)
}
fun fetchObjs() {
GlobalScope.launch(ioDispatcher) {
withContext(NonCancellable) {
val objs: List<Obj> = getObjs()
notify(objs)
}
}
}
private suspend fun notify(objs: List<Objs>) {
withContext(mainDispatcher) {
for(listener in listeners){
listener.onFetched(objs)
}
}
}
private fun getObjss(): List<Obj> {
/// fetch objects functionality
}
Testing, Inject The TestCoroutineDispatcher and use runBlockingTest
class FetchAllUseCase Test: FetchAllUseCase .Listener {
private lateinit var fetchAllUseCase: FetchAllUseCase
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
fetchAllUseCase = FetchAllUseCase(testDispatcher,testDispatcher)
fetchAllUseCase.registerListener(this)
}
#After
fun tearDown() {
fetchAllUseCase.unregisterListener(this)
}
#ExperimentalCoroutinesApi
#Test
fun fetchObjss()= runBlockingTest{
fetchAllUseCase.fetchObjs()
}
override fun onFetched(objs: List<Objs>) {
fail() //does not fail the test
}
If This Didn't work checkout this link Kotlin Coroutines in Android — Unit Test
Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-RxPaging
Here is my local unit test where I test a ViewModel while I am using Coroutines for networking :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
testCoroutineRule.runBlockingTest {
`when`(api.fetchShowList()).thenReturn(emptyList())
}
val repository = ShowRepository(dao, api, context, TestContextProvider())
testCoroutineRule.pauseDispatcher()
val viewModel = MainViewModel(repository)
assertThat(viewModel.shows.value, `is`(Resource.loading()))
testCoroutineRule.resumeDispatcher()
assertThat(viewModel.shows.value, `is`(Resource.success(emptyList())))
}
As you know I can pause and resume using TestCoroutineScope, so I can test when liveData is in Loading or Success state.
I wonder if we can do the same thing when we test while we are using RxJava.
At the moment I just can verify Success state :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character,
GetSpecieUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
}
}
}
in ViewModel init block, I send the network request. You can review it in the bellow class. That can be tested using pause and resume while using Coroutines. How about RxJava?
open class BaseViewModel<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val singleRequest: Single<T>
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
init {
sendRequest()
}
fun sendRequest() {
_liveData.value = Resource.Loading
singleRequest.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui()).subscribe({
_liveData.postValue(Resource.Success(it))
}) {
_liveData.postValue(Resource.Error(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
Without seeing what you tried, I can only guess there were two possible issues that required fixing:
Use the same TestScheduler for all provider methods:
class ImmediateSchedulerProvider : BaseSchedulerProvider {
val testScheduler = TestScheduler()
override fun computation(): Scheduler = testScheduler
override fun io(): Scheduler = testScheduler
override fun ui(): Scheduler = testScheduler
}
The unit tests weren't failing for the wrong state so they appear to pass even when the code hasn't run:
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character, GetSpecieUseCase(repository),
GetPlanetUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(Resource.Loading))
}
schedulerProvider.testScheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS) // <-------------
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
} else {
fail("Wrong type " + it) // <---------------------------------------------
}
}
}
I am testing a suspended method from my ViewModel that triggers LiveData to emit an object when coroutine is completed. When
I run each of those tests individually they pass, when I run them together always the first test fails. Surprisingly, when I run them in debug and I put break points at assertValue to check what the vaule is, both of the test pass. My guess is that the problem is with the state of LiveData or the whole PaymentViewModel. What am I doing wrong?
class PaymentViewModelTest : KoinTest {
private val paymentViewModel : PaymentViewModel by inject()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#Before
fun setup(){
Dispatchers.setMain(mainThreadSurrogate)
val modules = KoinModule()
startKoin {
androidContext(mock(Application::class.java))
modules(listOf(
modules.repositoryModule,
modules.businessModule,
modules.utilsModule)
)
}
declareMock<AnalyticsHelper>()
declareMock<Printer>()
}
#After
fun after(){
stopKoin()
Dispatchers.resetMain()
}
#Test
fun successfully_initializes_payment_flow() {
declareMock<PaymentRepository> {
runBlockingTest {
given(initPayment())
.willAnswer { InitPaymentResponse(0, PaymentStatus.INITIALIZED, 0) }
}
}
paymentViewModel.initPayment(BigDecimal(0))
paymentViewModel.paymentStatus.test()
.awaitValue()
.assertValue { value -> value.getContentIfNotHandled()?.data == PaymentStatus.INITIALIZED }
}
#Test
fun fails_to_initialize_payment_flow() {
declareMock<PaymentRepository> {
runBlockingTest {
given(initPayment())
.willThrow(MockitoKotlinException("", ConnectException()))
}
}
paymentViewModel.initPayment(BigDecimal(0))
paymentViewModel.paymentStatus.test()
.awaitValue()
.assertValue { value -> value.getContentIfNotHandled()?.status == ApiResponseStatus.ERROR}
}
}
Here is the method that I am testing:
fun initPayment(price: BigDecimal) {
paymentStatus.postValue(Event(ApiResponse.loading()))
viewModelScope.launch {
runCatching {
repository.initPayment()
}.onSuccess {
paymentSession = PaymentSession(it.paymentId)
paymentSession.price = price
postPaymentStatus(it.status)
}.onFailure {
postApiError(it)
}
}
}
private fun postPaymentStatus(status: PaymentStatus) =
paymentStatus.postValue(Event(ApiResponse.success(status)))
This might not be a complete answer because there is so much in your question. Start by trying to use a CoroutineTestRule:
#ExperimentalCoroutinesApi
class CoroutineTestRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun starting(description: Description?) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Your test will be something like:
class PaymentViewModelTest : KoinTest {
private val paymentViewModel : PaymentViewModel by inject()
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Before
fun setup(){
startKoin {
androidContext(mock(Application::class.java))
modules(
modules.repositoryModule,
modules.businessModule,
modules.utilsModule
)
}
declareMock<AnalyticsHelper>()
declareMock<Printer>()
}
#After
fun after(){
stopKoin()
}
// Other methods are the same.
}
You can use an AutoCloseKoinTest to remove that after() method.
You say that the test is passing when you run it isolated, so maybe this is enough. But there is more to dig into if this doesn't work. For example, I find it strange that you use runBlockingTest inside a mock and the assert is outside that block. Usually I would use MockK to mock suspending functions and test and assert any of them inside a runBlockingTest.
I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... } block doesn't actually wait for the code inside to finish, which is surprising to me.
The test fails because result is null. Why doesn't runBlocking { ... } run the launch block inside the ViewModel in blocking fashion?
I know if I convert it to a async method that returns a Deferred object, then I can get the object by calling await(), or I can return a Job and call join(). But, I'd like to do this by leaving my ViewModel methods as void functions, is there a way to do this?
// MyViewModel.kt
class MyViewModel(application: Application) : AndroidViewModel(application) {
val logic = Logic()
val myLiveData = MutableLiveData<Result>()
fun doSomething() {
viewModelScope.launch(MyDispatchers.Background) {
System.out.println("Calling work")
val result = logic.doWork()
System.out.println("Got result")
myLiveData.postValue(result)
System.out.println("Posted result")
}
}
private class Logic {
suspend fun doWork(): Result? {
return suspendCoroutine { cont ->
Network.getResultAsync(object : Callback<Result> {
override fun onSuccess(result: Result) {
cont.resume(result)
}
override fun onError(error: Throwable) {
cont.resumeWithException(error)
}
})
}
}
}
// MyViewModelTest.kt
#RunWith(RobolectricTestRunner::class)
class MyViewModelTest {
lateinit var viewModel: MyViewModel
#get:Rule
val rule: TestRule = InstantTaskExecutorRule()
#Before
fun init() {
viewModel = MyViewModel(ApplicationProvider.getApplicationContext())
}
#Test
fun testSomething() {
runBlocking {
System.out.println("Called doSomething")
viewModel.doSomething()
}
System.out.println("Getting result value")
val result = viewModel.myLiveData.value
System.out.println("Result value : $result")
assertNotNull(result) // Fails here
}
}
What you need to do is wrap your launching of a coroutine into a block with given dispatcher.
var ui: CoroutineDispatcher = Dispatchers.Main
var io: CoroutineDispatcher = Dispatchers.IO
var background: CoroutineDispatcher = Dispatchers.Default
fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(ui) {
block()
}
}
fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(io) {
block()
}
}
fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(background) {
block()
}
}
Notice ui, io and background at the top. Everything here is top-level + extension functions.
Then in viewModel you start your coroutine like this:
uiJob {
when (val result = fetchRubyContributorsUseCase.execute()) {
// ... handle result of suspend fun execute() here
}
And in test you need to call this method in #Before block:
#ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
ui = Dispatchers.Unconfined
io = Dispatchers.Unconfined
background = Dispatchers.Unconfined
}
(Which is much nicer to add to some base class like BaseViewModelTest)
As others mentioned, runblocking just blocks the coroutines launched in it's scope, it's separate from your viewModelScope.
What you could do is to inject your MyDispatchers.Background and set the mainDispatcher to use dispatchers.unconfined.
As #Gergely Hegedus mentions above, the CoroutineScope needs to be injected into the ViewModel. Using this strategy, the CoroutineScope is passed as an argument with a default null value for production. For unit tests the TestCoroutineScope will be used.
SomeUtils.kt
/**
* Configure CoroutineScope injection for production and testing.
*
* #receiver ViewModel provides viewModelScope for production
* #param coroutineScope null for production, injects TestCoroutineScope for unit tests
* #return CoroutineScope to launch coroutines on
*/
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
if (coroutineScope == null) this.viewModelScope
else coroutineScope
SomeViewModel.kt
class FeedViewModel(
private val coroutineScopeProvider: CoroutineScope? = null,
private val repository: FeedRepository
) : ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
fun getSomeData() {
repository.getSomeDataRequest().onEach {
// Some code here.
}.launchIn(coroutineScope)
}
}
SomeTest.kt
#ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val repository = mockkClass(FeedRepository::class)
private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)
override fun beforeAll(context: ExtensionContext?) {
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
// Reset Coroutine Dispatcher and Scope.
testDispatcher.cleanupTestCoroutines()
testScope.cleanupTestCoroutines()
}
#Test
fun topCafesPoc() = testDispatcher.runBlockingTest {
...
val viewModel = FeedViewModel(testScope, repository)
viewmodel.getSomeData()
...
}
}
I tried the top answer and worked, but I didn't want to go over all my launches and add a dispatcher reference to main or unconfined in my tests. So I ended up adding this code to my base testing class. I am defining my dispatcher as TestCoroutineDispatcher()
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
private val mainThreadDispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
Dispatchers.setMain(mainThreadDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
Dispatchers.resetMain()
}
}
in my base test class I have
#ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {
#BeforeAll
private fun doOnBeforeAll() {
MockitoAnnotations.initMocks(this)
}
}
I did use the mockk framework that helps to mock the viewModelScope instance like below
https://mockk.io/
viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
There are 3 steps that you need to follow.
Add dependency in gradle file.
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1")
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }
Create a Rule class MainCoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
#ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
Modify your test class to use ExperimentalCoroutinesApi runTest and advanceUntilIdle()
#OptIn(ExperimentalCoroutinesApi::class) // New addition
internal class ConnectionsViewModelTest {
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule() // New addition
...
#Test
fun test_abcd() {
runTest { // New addition
...
val viewModel = MyViewModel()
viewModel.foo()
advanceUntilIdle() // New addition
verify { mockObject.footlooseFunction() }
}
}
For explanation on why to do this you can always refer to the codelab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3
The problem you are having stems not from runBlocking, but rather from LiveData not propagating a value without an attached observer.
I have seen many ways of dealing with this, but the simplest is to just use observeForever and a CountDownLatch.
#Test
fun testSomething() {
runBlocking {
viewModel.doSomething()
}
val latch = CountDownLatch(1)
var result: String? = null
viewModel.myLiveData.observeForever {
result = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
assertNotNull(result)
}
This pattern is quite common and you are likely to see many projects with some variation of it as a function/method in some test utility class/file, e.g.
#Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> {
value = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
observeForever(observer)
removeObserver(observer)
return value
}
Which you can call like this:
val result = viewModel.myLiveData.getTestValue()
Other projects make it a part of their assertions library.
Here is a library someone wrote dedicated to LiveData testing.
You may also want to look into the Kotlin Coroutine CodeLab
Or the following projects:
https://github.com/googlesamples/android-sunflower
https://github.com/googlesamples/android-architecture-components
You don't have to change the ViewModel's code, the only change is required to properly set coroutine scope (and dispatcher) when putting ViewModel under test.
Add this to your unit test:
#get:Rule
open val coroutineTestRule = CoroutineTestRule()
#Before
fun injectTestCoroutineScope() {
// Inject TestCoroutineScope (coroutineTestRule itself is a TestCoroutineScope)
// to be used as ViewModel.viewModelScope fro the following reasons:
// 1. Let test fail if coroutine launched in ViewModel.viewModelScope throws exception;
// 2. Be able to advance time in tests with DelayController.
viewModel.injectScope(coroutineTestRule)
}
CoroutineTestRule.kt
#Suppress("EXPERIMENTAL_API_USAGE")
class CoroutineTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() {
val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher
override fun apply(
base: Statement,
description: Description?
) = object : Statement() {
override fun evaluate() {
Dispatchers.setMain(dispatcher)
base.evaluate()
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
}
The code will be executed sequentially (your test code, then view model code, then launched coroutine) due to the replaced main dispatcher.
The advantages of the approach above:
Write test code as normal, no need to use runBlocking or so;
Whenever a crash happen in coroutine, that will fail the test (because of cleanupTestCoroutines() called after every test).
You can test coroutine which uses delay internally. For that test code should be run in coroutineTestRule.runBlockingTest { } and advanceTimeBy() be used to move to the future.