Hi all :android-wave: , I am trying to test my fun...
# coroutines
a
Hi all 👋 , I am trying to test my function that throws an exception from a coroutine.
Copy code
private fun initViewModel() {
  viewModelScope.launch {
    getCurrentAccount() // Throws exception for certain conditions
  }
}
How to write UT for this method? I can find
assertFailsWith
, that is mentioned in some blogs. I am using
junit = "4.13.2"
.
solved 1
r
Launch the test with coroutine test's runTest, passing in either 'this' or 'backgroundScope' as the 'viewModelScope'
a
Hi @ross_a, Thanks for the response. I am already using
testScope
from
runTest
like this. Test Code
Copy code
@Test
fun `when initViewModel is called and currentAccountId is null, then IllegalStateException is thrown`() = runTest(
    timeout = 3.seconds,
    testBody = {
        val standardTestDispatcher = StandardTestDispatcher(
            scheduler = testScheduler,
        )
        val testScope = TestScope(
            context = standardTestDispatcher,
        )
        with(
            receiver = testScope,
        ) {
            // Mocks here
            
            val vm = MyViewModel(
                coroutineScope = coroutineScope,
                // Other dependencies here
            )

            assertThrows(IllegalStateException::class.java) {
                vm.initViewModel()
                advanceUntilIdle()
            }
        }
    },
)
I am overriding the
viewModelScope
as mentioned here. I am following this to inject the Coroutine scope in the view model and it overrides the
viewModelScope
https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.0 It is mentioned here that we can provide
CoroutineScope
as a parameter to
ViewModel
to override the
viewModelScope
.
d
You will probably need this: https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler Also, in general, coroutines launched in
viewModelScope
should not throw any exceptions, as this would crash the Android application.
a
In this specific use case, I am expecting the app to crash as this state should never occur. Thanks I will check the shared doc. thank you color
Hi, I tried using
CoroutineExceptionHandler
. But, it is returning the following error.
Copy code
A CoroutineExceptionHandler was passed to TestScope. Please pass it as an argument to a `launch` or `async` block on an already-created scope if uncaught exceptions require special treatment.
java.lang.IllegalArgumentException: A CoroutineExceptionHandler was passed to TestScope. Please pass it as an argument to a `launch` or `async` block on an already-created scope if uncaught exceptions require special treatment.
	at kotlinx.coroutines.test.TestScopeKt.TestScope(TestScope.kt:170)
	at com.makeappssimple.abhimanyu.coroutinesplayground.android.home.HomeScreenViewModelTest$crashApp$1.invokeSuspend(HomeScreenViewModelTest.kt:27)
	at com.makeappssimple.abhimanyu.coroutinesplayground.android.home.HomeScreenViewModelTest$crashApp$1.invoke(HomeScreenViewModelTest.kt)
	at com.makeappssimple.abhimanyu.coroutinesplayground.android.home.HomeScreenViewModelTest$crashApp$1.invoke(HomeScreenViewModelTest.kt)
To fix this, I changed the ViewModel method like this,
Copy code
fun crashApp(
        context: CoroutineContext = EmptyCoroutineContext,
): Job {
        return viewModelScope.launch(context) {
            println("Throwing exception")
            throw IllegalStateException("Crash App Test")
        }
}
And test method like this,
Copy code
@Test
fun crashApp() = runTest {
        val standardTestDispatcher: TestDispatcher = StandardTestDispatcher(
            scheduler = testScheduler,
        )
        val testScope = TestScope(
            context = standardTestDispatcher,
        )
        val homeScreenViewModel = HomeScreenViewModel(
            coroutineScope = testScope,
        )

        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            println("Caught exception: $throwable")
        }

        val job = homeScreenViewModel.crashApp(
            context = coroutineExceptionHandler,
        )
        job.join()

        println("Test completed")
}
But, the
CoroutineExceptionHandler
is not catching the exception.
d
Where does the
viewModelScope
come from? If it has a normal
Job
and not
SupervisorJob
, then that
Job
is responsible for processing exceptions, and
CoroutineExceptionHandler
will not be invoked;
CoroutineExceptionHandler
is only for the exceptions not handled by structured concurrency.
a
viewModelScope
is from the ViewModel that I am overriding like this,
Copy code
@HiltViewModel
public class HomeScreenViewModel @Inject constructor(
    @ApplicationScope coroutineScope: CoroutineScope,
) : ViewModel(
    viewModelScope = coroutineScope,
) {
    public fun crashApp(
        context: CoroutineContext = EmptyCoroutineContext,
    ): Job {
        return viewModelScope.launch(context) {
            println("Throwing exception")
            throw IllegalStateException("Crash App Test")
        }
    }
}
d
Ok, please try this:
Copy code
@Test
fun crashApp() = runTest {
        val standardTestDispatcher: TestDispatcher = StandardTestDispatcher(
            scheduler = testScheduler,
        )
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            println("Caught exception: $throwable")
        }
        val testScope = CoroutineScope( // <--- not a TestScope!
            standardTestDispatcher + SupervisorJob() + coroutineExceptionHandler // <--- SupervisorJob
        )
        val homeScreenViewModel = HomeScreenViewModel(
            coroutineScope = testScope,
        )
        val job = homeScreenViewModel.crashApp()
        job.join()

        println("Test completed")
}
a
This works as expected. 😄 . Did a couple of other changes as well to see how it behaves. 1. Using
TestScope
Copy code
val testScope = TestScope( // <--- TestScope!
  standardTestDispatcher + coroutineExceptionHandler + SupervisorJob() // <--- SupervisorJob
)
Error
Copy code
A CoroutineExceptionHandler was passed to TestScope. Please pass it as an argument to a `launch` or `async` block on an already-created scope if uncaught exceptions require special treatment.
2. Without
SupervisorJob
Copy code
val testScope = CoroutineScope( // <--- not a TestScope!
  standardTestDispatcher + coroutineExceptionHandler
)
This also works.
Is the
SupervisorJob
recommended if the test works without it as well?
d
It's recommended because a non-injected
ViewModelScope
also has a
SupervisorJob
, so with a
SupervisorJob
, you get the behavior that's closer to reality.
a
Cool. Thanks for the help. thank you color
🙂 1