Nino
03/29/2023, 8:36 PM@Test
fun passes() = runTest {
CoroutineScope(EmptyCoroutineContext).launch {
throw Exception()
}
}
This test passes (the exception is displayed in the console output tho)
@Test
fun fails() = runTest {
throw Exception()
}
This test fails
I would like to have my test failing when a launched coroutine fails... is it possible ?ephemient
03/29/2023, 9:14 PMGlobalScope.launch
or thread
.Patrick Steiger
03/29/2023, 9:29 PMCoroutineScope(Job(coroutineContext.job))
I believe it will failCLOVIS
03/29/2023, 9:47 PMrunTest
injects a CoroutineScope
, you can just call launch
directly:
@Test
fun test() = runTest {
launch {
throw Exception()
}
}
Dmitry Khalanskiy [JB]
03/30/2023, 3:32 AMNino
03/30/2023, 7:24 AMclass TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testCoroutineScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
throw throwable
}
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testCoroutineScope.runTest {
block()
}
}
Patrick Steiger
03/31/2023, 1:30 AMephemient
03/31/2023, 1:33 AMlifecycleScope
can repeatOnLifecycle
, which is better than viewModelScope
for most purposes - you don't want to be taking up resources while your UI is not in the foreground. so simply expose Flow
or suspend
from your ViewModel and let the UI use the appropriate scopeNino
03/31/2023, 9:13 AMrepeatOnLifecycle
).
Source: https://bladecoder.medium.com/kotlins-flow-in-viewmodels-it-s-complicated-556b472e281a
Example: https://github.com/NinoDLC/Kotlin_Flow_To_The_View
viewModelScope
should be used only for short-term operations that won't directly affect UI (writing to a database, sending logs / tracks to a server, etc). For anything related to the view, you can use instead liveData(CoroutineContext) {}
block in your ViewModel, it will be automatically cancelled 5 sec after the user put the app in background.
This way, no boilerplate, easy to test (both the LiveData and the public functions triggered by the view), no resource wastedPatrick Steiger
03/31/2023, 12:51 PMNino
03/31/2023, 5:08 PM@HiltViewModel // Hilt will inject this for me
class TasksViewModel @Inject constructor(
// Injected stuff like UseCase or Repository or whatever
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
) : ViewModel() {
val viewStateLiveData: LiveData<List<TasksViewStateItem>> = liveData(<http://coroutineDispatcherProvider.io|coroutineDispatcherProvider.io>) {
// you do your suspend or collect magic there to *emit()* value(s)
}
@Singleton
class CoroutineDispatcherProvider @Inject constructor() {
val main: CoroutineDispatcher = Dispatchers.Main
val io: CoroutineDispatcher = <http://Dispatchers.IO|Dispatchers.IO>
}
Test code:
class TasksViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val tasksViewModel = TasksViewModel(
// Inject mocks there
coroutineDispatcherProvider = testCoroutineRule.getTestCoroutineDispatcherProvider(),
)
@Before
fun setUp() {
// some mocking for nominal case goes there
}
@Test
fun `nominal case`() = testCoroutineRule.runTest {
// When
tasksViewModel.viewStateLiveData.observeForTesting(this) {
// Then
assertThat(it.value).isEqualTo(getDefaultTasksViewStateItems())
}
}
class TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testCoroutineScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Thread.setDefaultUncaughtExceptionHandler { _, e -> throw e }
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testCoroutineScope.runTest {
block()
}
fun getTestCoroutineDispatcherProvider() = mockk<CoroutineDispatcherProvider> {
every { main } returns testCoroutineDispatcher
every { io } returns testCoroutineDispatcher
}
}