Abhimanyu
10/13/2024, 3:53 AMprivate 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"
.ross_a
10/13/2024, 9:04 AMAbhimanyu
10/13/2024, 12:54 PMtestScope
from runTest
like this.
Test 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
.Dmitry Khalanskiy [JB]
10/14/2024, 1:21 PMviewModelScope
should not throw any exceptions, as this would crash the Android application.Abhimanyu
10/14/2024, 1:35 PMAbhimanyu
10/16/2024, 7:53 AMCoroutineExceptionHandler
.
But, it is returning the following error.
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,
fun crashApp(
context: CoroutineContext = EmptyCoroutineContext,
): Job {
return viewModelScope.launch(context) {
println("Throwing exception")
throw IllegalStateException("Crash App Test")
}
}
And test method like this,
@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.Abhimanyu
10/16/2024, 7:54 AMDmitry Khalanskiy [JB]
10/16/2024, 9:03 AMviewModelScope
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.Abhimanyu
10/16/2024, 9:04 AMviewModelScope
is from the ViewModel that I am overriding like this,
@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")
}
}
}
Dmitry Khalanskiy [JB]
10/16/2024, 9:07 AM@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")
}
Abhimanyu
10/16/2024, 9:13 AMTestScope
val testScope = TestScope( // <--- TestScope!
standardTestDispatcher + coroutineExceptionHandler + SupervisorJob() // <--- SupervisorJob
)
Error
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
val testScope = CoroutineScope( // <--- not a TestScope!
standardTestDispatcher + coroutineExceptionHandler
)
This also works.Abhimanyu
10/16/2024, 9:14 AMSupervisorJob
recommended if the test works without it as well?Dmitry Khalanskiy [JB]
10/16/2024, 9:15 AMViewModelScope
also has a SupervisorJob
, so with a SupervisorJob
, you get the behavior that's closer to reality.Abhimanyu
10/16/2024, 9:16 AM