Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 703 Vote(s) - 3.45 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Coroutines - unit testing viewModelScope.launch methods

#1
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
}
}

```
Reply

#2
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][1] someone wrote dedicated to LiveData testing.

You may also want to look into the [Kotlin Coroutine CodeLab][2]

Or the following projects:

[To see links please register here]


[To see links please register here]



[1]:

[To see links please register here]

[2]:

[To see links please register here]

Reply

#3
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.
Reply

#4
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)
Reply

#5
As _@Gergely Hegedus_ [mentions above][1], 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*

```Kotlin
/**
* 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*

```Kotlin
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*

```Kotlin
@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()
...
}
}
```


[1]:

[To see links please register here]

Reply

#6
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)
}
}
````
Reply

#7
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:
1. Write test code as normal, no need to use `runBlocking` or so;
2. Whenever a crash happen _in_ coroutine, that will fail the test (because of `cleanupTestCoroutines()` called after every test).
3. 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.
Reply

#8
I did use the mockk framework that helps to mock the viewModelScope instance like below

[To see links please register here]


viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
Reply

#9
There are 3 steps that you need to follow.
1. Add dependency in gradle file.

```
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1")
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }
```

2. Create a Rule class **MainCoroutineRule**
```import kotlinx.coroutines.Dispatchers
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()
}
}
```

3. 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

[To see links please register here]

Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through