J'ai un broadcastReceiver qui démarre une coroutine et j'essaye de tester cela unitaire ...
L'émission:
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)
}
}
Et voici le 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())
}
Ce qui se passe, c'est que la fonction que je vérifie n'est jamais appelée. Le test se termine avant le retour de la coroutine ... Je sais que GlobalScope
c'est mauvais à utiliser, mais je ne sais pas comment faire autrement.
EDIT 1: Si je mets un délai avant le verify
, cela semble fonctionner, car cela laisse le temps à la coroutine de se terminer et de revenir, cependant, je ne veux pas avoir de test reposant sur le retard / sommeil ... je pense que la solution est d'introduire correctement une portée au lieu de l'utiliser GlobalScope
et de la contrôler dans le test. Hélas, je n'ai aucune idée de la convention de déclaration des portées coroutines.
Je vois, vous devrez utiliser un Unconfined
répartiteur:
val Unconfined: CoroutineDispatcher (source)
Un répartiteur de coroutine qui n'est confiné à aucun thread spécifique. Il exécute la continuation initiale d'une coroutine dans la trame d'appel courante et laisse la coroutine reprendre dans n'importe quel thread utilisé par la fonction de suspension correspondante, sans imposer de politique de threading spécifique. Les coroutines imbriquées lancées dans ce répartiteur forment une boucle d'événements pour éviter les débordements de pile.
Échantillon de documentation:
withContext(Dispatcher.Unconfined) { println(1) withContext(Dispatcher.Unconfined) { // Nested unconfined println(2) } println(3) } println("Done")
Pour mes tests ViewModel, je passe un contexte coroutine au constructeur ViewModel afin que je puisse basculer entre Unconfined
et d'autres répartiteurs, par exemple Dispatchers.Main
et Dispatchers.IO
.
Contexte Coroutine pour les tests:
@ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
override val Main: CoroutineContext = Unconfined
override val IO: CoroutineContext = Unconfined
}
Contexte Coroutine pour l'implémentation actuelle de ViewModel:
open class CoroutineContextProvider {
open val Main: CoroutineContext by lazy { Dispatchers.Main }
open val IO: CoroutineContext by lazy { Dispatchers.IO }
}
VoirModèle:
@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()
}
}
}
}
À partir de la version coroutine-core, 1.2.1
vous pouvez utiliser runBlockingTest
:
Dépendances:
def coroutines_version = "1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
par exemple:
@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)
}
Cet article est collecté sur Internet, veuillez indiquer la source lors de la réimpression.
En cas d'infraction, veuillez [email protected] Supprimer.
laisse moi dire quelques mots