I have a ViewModel that launched with `viewModelScope.launch {}` a `while(isActive)` loop where I do...
l
I have a ViewModel that launched with
viewModelScope.launch {}
a
while(isActive)
loop where I do things. I’m trying to test that viewModel, mocking everything it needs. The test is run inside a
runTest
block. The test reaches the
assertEqual
line, which passes, but then it doesn’t cancel the
launch
and that keeps running (found it out while debugging). Am I doing something wrong?
s
did you replace main dispatcher? I.e. with JUnit extension
Copy code
class ReplaceMainDispatcherExtension(
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : BeforeEachCallback, AfterEachCallback {

    override fun beforeEach(context: ExtensionContext) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
    }
}
l
Yes that works, the test run jjust fine
Not missing main or anything
All other tests are passing, but this one which starts the loop, just won’t complete/cancel that when the test is over
s
Did you try to run such test with the TestScope instead of view model scope? You can pass it outside to check
l
No, haven’t, DMed you
z
l
Read them already, there’s nothing specific for my scenario
s
What I have proposed in DM:
Copy code
class MyViewModel: ViewModel() {

    fun runSomething(scope: CoroutinesScope = viewModelScope) {
        scope.launch {
            // your problematic code
        }
    }
}
Copy code
class MyViewModelTest {
    val scope = TestScope(UnconfinedTestDispatcher())
    
    @Test 
    fun test() {
        MyViewModel().runSomething(scope)
        scope.cancel()
    }
}
it works
z
It's not covered specifically, but the main idea is that the coroutine needs to be in a scope which is cancelled. If you use the coroutine testing library,
TestScope.backgroundScope
is something you can pass into the object you're testing, as coroutines in there will be cancelled at the end of
runTest
.
l
Is there a way to instruct
viewModelScope.launch
- without parameter - to be cancelled as well?
Something like a
@Rule
I can setup on my tests
z
Alternatively, if you want to keep using the Jetpack
viewModelScope
directly (which generally makes testing more difficult, unfortunately), you can attempt to clear the ViewModel at the end of the test, which would also cancel that scope. Not sure off the top of my head how difficult that is to achieve though.
If you're writing tests for these objects, I'd go for injecting a scope so that they can be easily replaced in tests.
l
I see, I recall on a previous project we used
viewModelScope
but provided dispatchers in the
launch
, using different ones for testing. Would that work as well?
z
You could technically solve it using contexts. If you were to inject a
Job
into the ViewModel that you then pass in as an extra parameter every time you call
viewModelScope.launch()
, then each of those newly created coroutines would become children of the injected
Job
, and cancelling the parent would cancel them as well. This would, however, mean that the coroutines are no longer tied to the ViewModel's lifecycle, so you'd need to also cancel the injected job whenever the ViewModel is cleared, for example like this https://developer.android.com/topic/libraries/architecture/viewmodel#clear-dependencies Having to remember to pass it in every time you launch a coroutine though, my opinion is that this isn't a clean way to do it. Ideally a scope where you're launching should already contain a correct configuration for its coroutines (dispatcher, error handler, and especially parent job), with an extra context parameter only required in rare cases.
👍 1
h
I agree with @zsmb especially because that's leveraging one of the selling points of a coroutine
e
Are you used to yield() inside the loop?
l
No, I don’t have any
yield
e
You should call it inside loop
l
for what?