package com.example import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test /** * Dependencies: implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61" def coroutines_version = '1.3.3' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation "junit:junit:4.12" */ @ExperimentalCoroutinesApi class ViewModelCoroutineDispatcherTest { private val dispatcher = TestCoroutineDispatcher() @Before fun before() { Dispatchers.setMain(dispatcher) } @After fun after() { Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } /* This test fails, because viewModelScope swallows the exception. However, under the hood viewModelScope uses Dispatchers.Main, which has been replaced by a TestCoroutineScope. How can we make this scope (re)throw its exception? */ @Test(expected = ExpectedException::class) fun `this fails`() = dispatcher.runBlockingTest { MyViewModel().launchSomething() } /* This test fails too. We don't need runBlockingTest, because we don't actually call any suspending functions or call its TestCoroutineScope receiver. Still, I'd expect the replaced Main dispatcher to run my coroutines synchronously and rethrow the first exception. Why doesn't it do this? */ @Test(expected = ExpectedException::class) fun `this fails too`() { MyViewModel().launchSomething() } /* This test succeeds, because the coroutine scope that launches is the TestCoroutineScope */ @Test(expected = ExpectedException::class) fun `this works`() = dispatcher.runBlockingTest { launch { throw ExpectedException } } } class MyViewModel : ViewModel() { fun launchSomething() { viewModelScope.launch { throw ExpectedException } } } object ExpectedException : Exception("expected")