Android-тест не может выполнить инструментализированные тесты фрагментов с ViewModel, предоставленной Dagger, возникает NullPointerException из-за параметра владельца ViewModelProvider.

Вопрос или проблема

У меня есть фрагмент, экземпляр ViewModel которого предоставляется компонентом, привязанным к графу навигации Главной активности. Компонент приложения Dagger был.injected в единственную Главную активность и захвачен внутри фрагмента как ленивый делегируемый переменная.

private val myViewModel: MyViewModel by lazy {
        ViewModelProvider(findNavController().getBackStackEntry(R.id.nav_graph), Factory(this) { stateHandle ->
            (activity as MainActivity).myComponent.myViewModel()
                .create(stateHandle)}).get(MyViewModel::class.java)
    }

Параметр ViewModelProvider.Factory метода ViewModelProvider использует метод create из интерфейса Factory, определенного в классе MyViewModel; этот интерфейс аннотирован @AssistedFactory, в то время как savedStateHandle аннотирован @AssistInject в конструкторе MyViewModel.

class MyViewModel @AssistedInject constructor(
    private val myRepository: MyRepository,
    @Assisted val savedStateHandle: SavedStateHandle
    ) : ViewModel() {

    @AssistedFactory
    interface MyViewModelFactory : ViewModelProvider.Factory {
        fun create(savedStateHandle: SavedStateHandle) : MyViewModel
    }

    // Другой код ViewModel 
}

Приложение работает нормально, и компонент приложения Dagger получает предоставленные экземпляры MyViewModel через свои ленивые делегаты, но теперь, когда я пытаюсь написать инструментальный тест для проверки некоторых полей интерфейса на самом фрагменте, я получаю исключение NullPointerException. В частности:

java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.lifecycle.ViewModelProvider.<init>, parameter owner
at androidx.lifecycle.ViewModelProvider.<init>(Unknown Source:2)

Это говорит мне о том, что параметр owner, findNavController().getBackStackEntry(R.id.nav_graph) вызывает ошибку NPE, но я не знаю, как это решить, чтобы мой тест мог скомпилироваться и выполниться.

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

@RunWith(AndroidJUnit4::class)
class MyFragmentTest {

    @get:Rule
    var activityRule = ActivityScenarioRule(MainActivity::class.java)

    private lateinit var scenario: FragmentScenario<MyFragment>
    private lateinit var binding: FragmentMyBinding
    private lateinit var mockNavController : NavController
    
    @Test
    fun setUp() {
        mockNavController = mock(NavController::class.java)
        scenario = launchFragmentInContainer<MyFragment> {
            MyFragment().also { fragment ->
                fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                    if (viewLifecycleOwner != null) {
                        // Вид фрагмента только что был создан
                        mockNavController.setGraph(R.navigation.nav_graph)
                        Navigation.setViewNavController(fragment.requireView(), mockNavController)
                    }
                }
            }
        }
    }

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

Ответ или решение

Для решения проблемы с NullPointerException при тестировании фрагментов с использованием ViewModel, предоставляемых Dagger, необходимо учесть несколько аспектов.

Проблема

Ошибка java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.lifecycle.ViewModelProvider.<init>, parameter owner указывает на то, что при вызове ViewModelProvider параметр owner (в данном случае, findNavController().getBackStackEntry(R.id.nav_graph)) является null. Это говорит о том, что навигационный контроллер и его график не были должным образом настроены в вашем тесте.

Решение

  1. Настройка навигационного контроллера: Вам необходимо убедиться, что навигационный график и контроллер настроены до вызова ViewModelProvider.

  2. Изменение теста: Вам не нужно напрямую использовать viewLifecycleOwnerLiveData. Вместо этого вы можете сразу установить навигационный контроллер через launchFragmentInContainer и убедиться, что он корректно настроен.

Вот пример вашего теста с изменениями:

@RunWith(AndroidJUnit4::class)
class MyFragmentTest {

    @get:Rule
    var activityRule = ActivityScenarioRule(MainActivity::class.java)

    private lateinit var scenario: FragmentScenario<MyFragment>
    private lateinit var mockNavController: NavController

    @Before
    fun setUp() {
        mockNavController = mock(NavController::class.java)
        `when`(mockNavController.graph).thenReturn(mock(NavGraph::class.java))

        scenario = launchFragmentInContainer<MyFragment> {
            MyFragment().also { fragment ->
                val navHostFragment = mock(NavHostFragment::class.java)
                navHostFragment.setGraph(R.navigation.nav_graph)
                Navigation.setViewNavController(fragment.requireView(), mockNavController)
                fragment.requireActivity().supportFragmentManager.beginTransaction()
                    .replace(android.R.id.content, navHostFragment)
                    .commitNow()
            }
        }
    }

    @Test
    fun testMyFragmentUI() {
        // Ваши тесты UI для MyFragment
    }
}

Объяснение изменений

  1. Передача mockNavController: Теперь перед созданием MyFragment мы устанавливаем mockNavController прямо в фрагмент, тем самым устраняя возможность возникновения NPE.

  2. Использование @Before: Перенос настройки в метод @Before, чтобы код выполнялся перед каждым тестом, обеспечивая чистое состояние.

  3. Настройка графа навигации: Убедитесь, что граф навигации правильно инициализирован и установлен перед обращением к ViewModel.

Заключение

Проблема возникла из-за отсутствия корректной инициализации навигационного контроллера в тестах. Убедитесь, что навигационные компоненты настроены правильно, и ваше тестирование не будет сталкиваться с NullPointerExceptions, что позволит вам продолжить работу с Dagger и сохранить структуру кода.

Оцените материал
Добавить комментарий

Капча загружается...