Android Kotlin Coroutine UnitТестинг - PullRequest
1 голос
/ 10 июня 2019

У меня есть широковещательный приемник, который запускает сопрограмму, и я пытаюсь выполнить модульное тестирование этого ...

Трансляция:

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)
    }
}

А вот тест:

@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())
}

Что происходит, так это то, что проверяемая мной функция никогда не вызывается.Тест заканчивается до возвращения сопрограммы ... Я знаю, что GlobalScope плохо использовать, но я не уверен, как еще это сделать.

РЕДАКТИРОВАТЬ 1: Если я поставлю задержку перед verify, она, похоже, сработает, поскольку дает время для завершения и возврата сопрограммы, однако я не хочу, чтобы тест полагался на задержку/ сон ... Я думаю, что решение состоит в том, чтобы правильно ввести область вместо использования GlobalScope и контролировать это в тесте.Увы, я не имею ни малейшего понятия, что такое соглашение об объявлении объема сопрограмм.

Ответы [ 2 ]

3 голосов
/ 11 июня 2019

Понятно, вам придется использовать диспетчер Unconfined:

val Unconfined: CoroutineDispatcher (source)

Диспетчер сопрограмм, не привязанный к какой-либо конкретной теме. Он выполняет начальное продолжение сопрограммы в текущем кадре вызова и позволяет возобновить сопрограмму в любом потоке, который используется соответствующей функцией приостановки, без необходимости какой-либо конкретной политики потоков. Вложенные сопрограммы, запущенные в этом диспетчере, образуют цикл обработки событий, чтобы избежать переполнения стека.

Образец документации:

withContext(Dispatcher.Unconfined) {
   println(1)
   withContext(Dispatcher.Unconfined) { // Nested unconfined
       println(2)
   }
   println(3)
}
println("Done")

Для моих тестов ViewModel я передаю контекст сопрограммы конструктору ViewModel, чтобы я мог переключаться между Unconfined и другими диспетчерами, например. Dispatchers.Main и Dispatchers.IO.

Контекст сопрограммы для тестов:

@ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
    override val Main: CoroutineContext = Unconfined
    override val IO: CoroutineContext = Unconfined
}

Контекст сопрограммы для фактической реализации ViewModel:

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()
            }
        }
    }

}

Обновление

Начиная с версии сопрограммы 1.2.1 вы можете использовать runBlockingTest:

Зависимость:

def coroutines_version = "1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"

например:

@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)
}
0 голосов
/ 11 июня 2019

Да, как упоминал Родриго Кейроз, блокировка запуска решит проблему.

@Test
fun onReceive_ValidAlarm_StartsTriggerActivity() = runBlockingTest {
    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())
}
...