Сопрограммы - модульное тестирование методов viewModelScope.launch - PullRequest
4 голосов
/ 19 апреля 2019

Я пишу юнит-тесты для моей модели viewModel, но у меня возникают проблемы при выполнении тестов.Блок runBlocking { ... } на самом деле не ожидает завершения кода внутри, что меня удивляет.

Тест не пройден, потому что result равен null.Почему runBlocking { ... } не запускает блок launch внутри ViewModel в режиме блокировки?

Я знаю, если я преобразую его в async метод, который возвращает объект Deferred, тогда я могу получитьобъект, вызвав await(), или я могу вернуть Job и вызвать join(). Но , я бы хотел сделать это, оставив методы ViewModel как void функции, есть ли способ сделать это?

// MyViewModel.kt

class MyViewModel(application: Application) : AndroidViewModel(application) {

    val logic = Logic()
    val myLiveData = MutableLiveData<Result>()

    fun doSomething() {
        viewModelScope.launch(MyDispatchers.Background) {
            System.out.println("Calling work")
            val result = logic.doWork()
            System.out.println("Got result")
            myLiveData.postValue(result)
            System.out.println("Posted result")
        }
    }

    private class Logic {
        suspend fun doWork(): Result? {
          return suspendCoroutine { cont ->
              Network.getResultAsync(object : Callback<Result> {
                      override fun onSuccess(result: Result) {
                          cont.resume(result)
                      }

                     override fun onError(error: Throwable) {
                          cont.resumeWithException(error)
                      }
                  })
          }
    }
}
// MyViewModelTest.kt

@RunWith(RobolectricTestRunner::class)
class MyViewModelTest {

    lateinit var viewModel: MyViewModel

    @get:Rule
    val rule: TestRule = InstantTaskExecutorRule()

    @Before
    fun init() {
        viewModel = MyViewModel(ApplicationProvider.getApplicationContext())
    }

    @Test
    fun testSomething() {
        runBlocking {
            System.out.println("Called doSomething")
            viewModel.doSomething()
        }
        System.out.println("Getting result value")
        val result = viewModel.myLiveData.value
        System.out.println("Result value : $result")
        assertNotNull(result) // Fails here
    }
}

Ответы [ 3 ]

1 голос
/ 17 июня 2019

Как уже упоминалось, блокирование выполнения просто блокирует сопрограммы, запущенные в его области действия, отдельно от вашего viewModelScope.Что вы можете сделать, это внедрить свой MyDispatchers.Background и настроить mainDispatcher на использование dispatchers.unconfined.

0 голосов
/ 15 июля 2019

Что вам нужно сделать, это обернуть запуск сопрограммы в блок с указанным диспетчером.

var ui: CoroutineDispatcher = Dispatchers.Main
var io: CoroutineDispatcher =  Dispatchers.IO
var background: CoroutineDispatcher = Dispatchers.Default

fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(ui) {
        block()
    }
}

fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(io) {
        block()
    }
}

fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(background) {
        block()
    }
}

Обратите внимание на пользовательский интерфейс, фоновый режим и фон сверху.Все здесь - верхний уровень + функции расширения.

Затем в viewModel вы запускаете свою сопрограмму следующим образом:

uiJob {
    when (val result = fetchRubyContributorsUseCase.execute()) {
    // ... handle result of suspend fun execute() here         
}

И в тесте вам нужно вызвать этот метод в блоке @Before:

@ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
    ui = Dispatchers.Unconfined
    io = Dispatchers.Unconfined
    background = Dispatchers.Unconfined
}

(что гораздо приятнее добавить в некоторый базовый класс, например BaseViewModelTest)

0 голосов
/ 23 апреля 2019

Проблема, с которой вы столкнулись, связана не с runBlocking, а с LiveData, не передающим значение без подключенного наблюдателя.

Я видел много способов справиться с этим, но самое простое - просто использовать observeForever и CountDownLatch.

@Test
fun testSomething() {
    runBlocking {
        viewModel.doSomething()
    }
    val latch = CountDownLatch(1)
    var result: String? = null
    viewModel.myLiveData.observeForever {
        result = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    assertNotNull(result)
}

Этот шаблон довольно распространен, и вы, вероятно, увидите множество проектов с некоторым его изменением в виде функции / метода в некотором классе / файле тестовой утилиты, например,

@Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
    var value: T? = null
    val latch = CountDownLatch(1)
    val observer = Observer<T> {
        value = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    observeForever(observer)
    removeObserver(observer)
    return value
}

Который вы можете назвать так:

val result = viewModel.myLiveData.getTestValue()

Другие проекты делают его частью библиотеки утверждений.

Вот библиотека кто-то написал посвященный тестированию LiveData.

Вы также можете заглянуть в Kotlin Coroutine CodeLab

Или следующие проекты:

https://github.com/googlesamples/android-sunflower

https://github.com/googlesamples/android-architecture-components

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...