Как обеспечить вызов ViewModel # onCleared в модульном тесте Android?

Это мой тестовый класс MWE, который зависит от AndroidX, JUnit 4 и MockK 1.9:

class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        MyViewModel::class.members
            .single { it.name == "onCleared" }
            .apply { isAccessible = true }
            .call(MyViewModel())

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

Примечание: метод защищен суперклассом ViewModel.

Я хочу убедиться, что MyViewModel#onCleared вызывает Object#function. В приведенном выше коде это достигается путем отражения. У меня вопрос: могу ли я как-то запустить или смоделировать систему Android, чтобы был вызван метод onCleared, чтобы мне не понадобилось отражение?

Из onCleared JavaDoc:

This method will be called when this ViewModel is no longer used and will be destroyed.

Другими словами, как мне создать эту ситуацию, чтобы я знал, что вызывается onCleared, и мог проверить его поведение?

Вы могли бы public override fun onCleared(), но это раскрывает метод, который не подходит, так как метод должен вызываться только системой Android.

Erik 09.01.2019 20:14
10
1
4 124
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

TL; DR

В этом ответе Robolectric используется для того, чтобы фреймворк Android вызывал onCleared на вашем ViewModel. Этот способ тестирования медленнее, чем использование отражения (как в вопросе), и зависит как от Robolectric, так и от платформы Android. Этот компромисс зависит от вас.


Глядя на исходный код Android ...

... вы можете видеть, что ViewModel#onCleared вызывается только в ViewModelStore (для вашего собственного ViewModels). Это класс хранения для моделей представлений и принадлежит классам ViewModelStoreOwner, например. FragmentActivity. Итак, когда ViewModelStore вызывает onCleared на вашем ViewModel?

В нем должен храниться ваш ViewModel, затем его нужно очистить (что вы не можете сделать самостоятельно).

Ваша модель представления сохраняется в ViewModelProvider, когда вы get на ViewModel с помощью ViewModelProviders.of(FragmentActivity activity).get(Class<T> modelClass), где T - это класс вашей модели представления. Он хранит его в ViewModelStoreFragmentActivity.

Хранилище ясно, например, когда ваша активность фрагмента уничтожена. Это куча связанных вызовов, которые встречаются повсюду, но в основном это:

  1. У вас есть FragmentActivity.
  2. Получите его ViewModelProvider, используя ViewModelProviders#of.
  3. Получите свой ViewModel с помощью ViewModelProvider#get.
  4. Разрушьте свою деятельность.

Теперь onCleared должен быть вызван в вашей модели представления. Давайте протестируем это с помощью Robolectric 4, JUnit 4, MockK 1.9:

  1. Добавьте @RunWith(RobolectricTestRunner::class) в свой тестовый класс.
  2. Создайте контроллер активности с помощью Robolectric.buildActivity(FragmentActivity::class.java)
  3. Инициализируйте действие с помощью setup на контроллере, это позволяет его уничтожить.
  4. Получите активность с помощью метода контроллера get.
  5. Получите свою модель представления, выполнив шаги, описанные выше.
  6. Уничтожьте активность с помощью destroy на контроллере.
  7. Проверьте поведение onCleared.

Полный пример класса ...

... на основе примера вопроса:

@RunWith(RobolectricTestRunner::class)
class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        val controller = Robolectric.buildActivity(FragmentActivity::class.java).setup()

        ViewModelProviders.of(controller.get()).get(MyViewModel::class.java)

        controller.destroy()

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

Я только что создал это расширение для ViewModel:

/**
 * Will create new [ViewModelStore], add view model into it using [ViewModelProvider]
 * and then call [ViewModelStore.clear], that will cause [ViewModel.onCleared] to be called
 */
fun ViewModel.callOnCleared() {
    val viewModelStore = ViewModelStore()
    val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = this@callOnCleared as T
    })
    viewModelProvider.get(this@callOnCleared::class.java)

    //Run 2
    viewModelStore.clear()//To call clear() in ViewModel
}

Это зависит от ViewModelStore#clear для вызова ViewModel#onCleared, и поэтому этот вызов несколько скрыт среди этих строк кода. Тем не менее, это решение довольно хорошее из-за его краткости, отсутствия необходимости в отражении или тестах Robolectric / Android, а также того факта, что на Android обычно вызов выполняется экземпляром ViewModelStore. Спасибо за Ваш ответ!

Erik 02.08.2019 18:14

В kotlin вы можете переопределить защищенную видимость с помощью public, а затем вызвать ее из теста.

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}

Немного @RestrictTo(RestrictTo.Scope.TESTS) поверх onCleared() и все хорошо

Charly Lafon 06.07.2020 11:53

Для Java, если вы создаете свой тестовый класс в том же пакете (в тестовом каталоге), что и класс ViewModel (здесь MyViewModel), то вы можете вызвать метод onCleared из тестового класса; поскольку защищенные методы также являются частными для пакета.

Да, на Java так работает. Но я не использую Java.

Erik 12.10.2021 16:08

Да, я знаю, это для пользователей, которые ищут решение на Java; поскольку в названии конкретно не упоминается «в Котлине».

Asdinesh 12.10.2021 16:17

Другие вопросы по теме

Как внедрить компонент Spring в тестовый класс JUnit 5, написанный на Kotlin?
Gmock, как указать, что никакие другие методы издевательства не должны вызываться?
Ничего не найдено для публикации. Модульные тесты в сборке Azure DevOps
Как протестировать метод, вызываемый внутри экземпляра компонента, с помощью Jest и Enzyme
Как упростить тестирование этой функции?
Подписка на наблюдаемое не определена только при запуске теста Angular Jasmine, но определяется при запуске самого приложения
Перенос модульного теста настраиваемого атрибута фильтра из .NET Framework в .NET Core 2.1
Тестирование эффектов NGRX - как проверить авторизацию при входе в систему
Как использовать макет одного класса внутри другого, чтобы JUNIT протестировал метод void для сбора аргументов?
Как установить локальное состояние компонента при тестировании с помощью библиотеки jest и react-testing-library?