I've got a broadcastReceiver that starts a coroutine and I am trying to unit test that...
The broadcast:
class AlarmBroadcastReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Timber.d("Starting alarm from broadcast receiver")
//inject(context) Don't worry about this, it's mocked out
GlobalScope.launch {
val alarm = getAlarm(intent)
startTriggerActivity(alarm, context)
}
}
private suspend fun getAlarm(intent: Intent?): Alarm {
val alarmId = intent?.getIntExtra(AndroidAlarmService.ALARM_ID_KEY, -1)
if (alarmId == null || alarmId < 0) {
throw RuntimeException("Cannot start an alarm with an invalid ID.")
}
return withContext(Dispatchers.IO) {
alarmRepository.getAlarmById(alarmId)
}
}
And here's the test:
@Test
fun onReceive_ValidAlarm_StartsTriggerActivity() {
val alarm = Alarm().apply { id = 100 }
val intent: Intent = mock {
on { getIntExtra(any(), any()) }.thenReturn(alarm.id)
}
whenever(alarmRepository.getAlarmById(alarm.id)).thenReturn(alarm)
alarmBroadcastReceiver.onReceive(context, intent)
verify(context).startActivity(any())
}
What's happening is that the function I'm verifying is never being called. The test ends before the coroutine returns... I'm aware that GlobalScope is bad to use, but I'm not sure how else to do it.
EDIT 1:
If I put a delay before the verify, it seems to work, as it allows time for the coroutine to finish and return, however, I don't want to have test relying on delay/sleep... I think the solution is to properly introduce a scope instead of using GlobalScope and control that in the test. Alas, I have no clue what is the convention for declaring coroutine scopes.
I see, You will have to use an Unconfined dispatcher:
val Unconfined: CoroutineDispatcher (source)A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without mandating any specific threading policy. Nested coroutines launched in this dispatcher form an event-loop to avoid stack overflows.
Documentation sample:
withContext(Dispatcher.Unconfined) { println(1) withContext(Dispatcher.Unconfined) { // Nested unconfined println(2) } println(3) } println("Done")
For my ViewModel tests, I pass a coroutine context to the ViewModel constructor so that I can switch between Unconfined and other dispatchers e.g. Dispatchers.Main and Dispatchers.IO.
Coroutine context for tests:
@ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
override val Main: CoroutineContext = Unconfined
override val IO: CoroutineContext = Unconfined
}
Coroutine context for the actual ViewModel implementation:
open class CoroutineContextProvider {
open val Main: CoroutineContext by lazy { Dispatchers.Main }
open val IO: CoroutineContext by lazy { Dispatchers.IO }
}
ViewModel:
@OpenForTesting
class SampleViewModel @Inject constructor(
val coroutineContextProvider: CoroutineContextProvider
) : ViewModel(), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext = job + coroutineContextProvider.Main
override fun onCleared() = job.cancel()
fun fetchData() {
launch {
val response = withContext(coroutineContextProvider.IO) {
repository.fetchData()
}
}
}
}
As of coroutine-core version 1.2.1 you can use runBlockingTest:
Dependencies:
def coroutines_version = "1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
e.g:
@Test
fun `sendViewState() sends displayError`(): Unit = runBlockingTest {
Dispatchers.setMain(Dispatchers.Unconfined)
val apiResponse = ApiResponse.success(data)
whenever(repository.fetchData()).thenReturn(apiResponse)
viewModel.viewState.observeForever(observer)
viewModel.processData()
verify(observer).onChanged(expectedViewStateSubmitError)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With