Hello, is it a feature that an exception thrown in...
# coroutines
n
Hello, is it a feature that an exception thrown inside a `launch`ed coroutine doesn't make a unit test (JUnit 4) fails ?
Copy code
@Test
fun passes() = runTest {
    CoroutineScope(EmptyCoroutineContext).launch {
        throw Exception()
    }
}
This test passes (the exception is displayed in the console output tho)
Copy code
@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 ?
e
not sure what else you expect from breaking structured concurrency, it's like
GlobalScope.launch
or
thread
.
p
If you make the first example
CoroutineScope(Job(coroutineContext.job))
I believe it will fail
c
runTest
injects a
CoroutineScope
, you can just call
launch
directly:
Copy code
@Test
fun test() = runTest {
    launch {
        throw Exception()
    }
}
d
Fixed in 1.7.0-Beta.
You can adapt your tests for these kinds of exceptions without upgrading, though, see https://github.com/Kotlin/kotlinx.coroutines/issues/1205#issuecomment-1232015300 and the surrounding discussion.
n
Sorry for the 3 first repliers, I wasn't explicit enough, I was asking that because it was working before and I don't know when it broke (I guess 1.6 ?) Using launch directly is not a possibility because in Android we use a special scope (viewModelScope) that is automatically cancelled when we leave the screen associated to the ViewModel.
For anyone interested, this is my updated TestCoroutineRule that is used in every coroutine-based UT that will fail a UT if an uncatched exception is thrown inside a coroutine :
Copy code
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 { _, 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()
    }
}
p
Side comment: Honestly viewModelScope is bad API, you can take in a CoroutineScope in constructor and pass it in as a Closeable to the super class ViewModel. In tests, create a scope that is child of runTest and pass it to your ViewModel
e
additionally,
lifecycleScope
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 scope
n
@Patrick Steiger It adds so much boilerplate code (both in source code and test code) for no real benefits (can you add on why viewModelScope extension is a bad API ?)... @ephemient It's still unsafe to collect a flow from the view (even with
repeatOnLifecycle
). 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 wasted
p
@Nino I don’t find it much boilerplate at all. I’m fact, it reduces testing boilerplate. Cleaner testing is good enough benefit for me (no need for Dispatchers.setMain API), because of no hard dependency on Dispatchers.Main as viewModelScope has. Also you can construct your scope however you like (custom coroutine context elements) There’s no way around lifecycleScope on activities as activities just cannot be constructor injected but in the case of ViewModel which we are in control of construction (through view model factories), it’s always better to do constructor injection and there’s no good reason to use viewModelScope in there IMO
n
I would like to see a snippet of code to be able to compare the boilerplate, because afaik injecting the scope would cause much more boilerplate, both in the VM and the UT. Here's how I do: Source code:
Copy code
@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)
    }
Copy code
@Singleton
class CoroutineDispatcherProvider @Inject constructor() {
    val main: CoroutineDispatcher = Dispatchers.Main
    val io: CoroutineDispatcher = <http://Dispatchers.IO|Dispatchers.IO>
}
Test code:
Copy 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())
        }
    }
Copy code
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
    }
}