Я пытаюсь протестировать некоторый код в своей модели представления. Я пытаюсь сделать два сетевых вызова с асинхронным диспетчером, но не понял, как это проверить. Это упрощенный пример того, что я по сути пытаюсь сделать:
// View Model Function
fun initialise() {
viewModelScope.launch(coroutineExceptionHandler) {
val accountDeferred = async(Dispatchers.IO) { accountModel.getAccounts() }
val contentDeferred = async(Dispatchers.IO) { contentModel.getContent() }
val account = accountDeferred.await()
val content = contentDeferred.await()
handleResult(account, content) // viewState is updated
}
}
// Unit Test
// Simplified
class Test {
@get:Rule
val coroutineRule = CoroutineTestRule(StandardTestDispatcher())
@Test
fun `This is the test`() {
runTest{
whenever(accountModel.getAccounts()).thenReturn(
Result.success( getAccountContent() , ReasonStatus.empty()))
whenever(contentModel.getContent()).thenReturn(
Result.success( getContent(), ReasonStatus.empty()))
// start view model
viewmodel = ViewModel(accountModel, contentModel)
runCurrent()
// In debugging mode, the variable viewState is set, before the two jobs in the viewmodel finish
val viewState = viewmodel.viewState.value
// check contents
assertNotEmpty(viewState.accounts)
// always fails since the network calls have not completed
}
}
}
Какие могут быть предложения по обеспечению того, чтобы два асинхронных ожидания выполнялись до того, как другой код в блоке runTest будет выполнен первым надежным образом?
Попробуйте заменить runCurrent()
на advanceUntilIdle()
.
По сути, он будет запускать все остальные сопрограммы в планировщике до тех пор, пока в очереди ничего не останется. Это хороший выбор по умолчанию, позволяющий запускать все ожидающие сопрограммы, и он будет работать в большинстве тестовых сценариев. Об этом говорится в документации Google. Я нашел это здесь https://developer.android.com/kotlin/coroutines/test
Вам необходимо использовать внедрение зависимостей, чтобы предоставить диспетчер ввода-вывода вашему ViewModel
, чтобы сделать его тестируемым. Тогда вы можете использовать внедренный диспетчер вместо жесткого кодирования Dispatchers.IO
:
class ViewModel(
//...
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
//...
fun initialise() {
viewModelScope.launch(coroutineExceptionHandler) {
val accountDeferred = async(ioDispatcher) { accountModel.getAccounts() }
val contentDeferred = async(ioDispatcher) { contentModel.getContent() }
Из документации по тестированию сопрограмм:
Если основной диспетчер был заменен на TestDispatcher, все вновь созданные TestDispatcher будут автоматически использовать планировщик из главного диспетчера, включая StandardTestDispatcher, созданный runTest, если ему не передан другой диспетчер.
Это упрощает обеспечение использования только одного планировщика во время теста. Чтобы это работало, обязательно создайте все остальные экземпляры TestDispatcher после вызова Dispatchers.setMain.
Предполагая, что CoroutineTestRule
установил диспетчер Main
, теперь вы можете создать новый TestDispatcher
для внедрения, и он будет использовать тот же планировщик:
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `This is the test`() = runTest {
val dispatcher = StandardTestDispatcher()
val viewmodel = ViewModel(/*other args*/, dispatcher)
advanceUntilIdle()
//...
}
Спасибо, это то, что я в итоге сделал, но делегировал ввод-вывод на уровень бизнес-уровня.
Это зависит от ваших требований к экземпляру. Рассмотрите возможность использования
supervisorScope
,joinAll
илиadvanceUntilIdle
. Проверьте их реализацию и поймите, какой из них подходит вам лучше всего.