FragmentScenario не работает должным образом

Я использую почти ту же архитектуру, что и образец Google: GithubBrowserSample.

Это поле введено в мой класс Fragment:

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

val viewModel: TrainingListViewModel by viewModels {
    viewModelFactory
}

Во всех моих тестах фрагментов я получал эту ошибку, которая соответствует полю, которое создается путем инъекции:

java.lang.RuntimeException: kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
at androidx.test.runner.MonitoringInstrumentation.runOnMainSync(MonitoringInstrumentation.java:441)
at androidx.test.core.app.ActivityScenario.onActivity(ActivityScenario.java:564)
at androidx.fragment.app.testing.FragmentScenario.internalLaunch(FragmentScenario.java:300)
at androidx.fragment.app.testing.FragmentScenario.launchInContainer(FragmentScenario.java:282)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragmentTest.init(TrainingListFragmentTest.kt:213)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at androidx.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:76)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment.getViewModelFactory(TrainingListFragment.kt:41)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment$viewModel$2.invoke(TrainingListFragment.kt:49)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment$viewModel$2.invoke(TrainingListFragment.kt:39)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:52)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment.getViewModel(Unknown Source:7)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragmentTest$init$$inlined$launchFragmentInContainer$1.instantiate(FragmentScenario.kt:114)
at androidx.fragment.app.testing.FragmentScenario$1.perform(FragmentScenario.java:310)
at androidx.fragment.app.testing.FragmentScenario$1.perform(FragmentScenario.java:301)
at androidx.test.core.app.ActivityScenario.lambda$onActivity$2$ActivityScenario(ActivityScenario.java:551)
at androidx.test.core.app.ActivityScenario$$Lambda$4.run(Unknown Source:4)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at android.app.Instrumentation$SyncRunnable.run(Instrumentation.java:2207)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Это происходит, когда я устанавливаю свой фрагмент таким образом с помощью FragmentScenario:

viewModel = mock(TrainingListViewModel::class.java)

val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
    TrainingListFragment().apply {
        appExecutors = countingAppExecutors.appExecutors
        viewModelFactory = ViewModelUtil.createFor(viewModel)
    }
}

dataBindingIdlingResourceRule.monitorFragment(scenario)
scenario.onFragment { fragment ->
    Navigation.setViewNavController(fragment.requireView(), navController)
    fragment.binding.trainingListRv.itemAnimator = null
    fragment.disableProgressBarAnimations()
}

Но когда я настраиваю фрагмент перед использованием его с FragmentScenario, он работает:

viewModel = mock(TrainingListViewModel::class.java)
val trainingListFragment = TrainingListFragment()
trainingListFragment.viewModelFactory = ViewModelUtil.createFor(viewModel)
trainingListFragment.appExecutors = countingAppExecutors.appExecutors

trainingListFragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
    if (viewLifecycleOwner != null) {
        Navigation.setViewNavController(trainingListFragment.requireView(), navController)
    }
}

val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
    trainingListFragment
}

Вот исходный код моего теста:

@RunWith(AndroidJUnit4::class)
class TrainingListFragmentTest {
    @Rule
    @JvmField
    val executorRule = TaskExecutorWithIdlingResourceRule()

    @Rule
    @JvmField
    val countingAppExecutors = CountingAppExecutorsRule()

    @Rule
    @JvmField
    val dataBindingIdlingResourceRule = DataBindingIdlingResourceRule()

    private val navController = mock<NavController>()
    private val trainingsLiveData = MutableLiveData<Resource<List<Training>>>()
    private val isPremiumLiveData = MutableLiveData<PremiumAccount>()
    private val trainingDeletedLiveData = MutableLiveData<Event<Boolean>>()
    private val navigateToCreateTrainingLiveData = MutableLiveData<Event<Boolean>>()
    private val navigateToExerciseListLiveData = MutableLiveData<Event<Training>>()
    private lateinit var viewModel: TrainingListViewModel

    @Before
    fun init() {
        viewModel = mock(TrainingListViewModel::class.java)
        val trainingListFragment = TrainingListFragment()
        trainingListFragment.viewModelFactory = ViewModelUtil.createFor(viewModel)
        trainingListFragment.appExecutors = countingAppExecutors.appExecutors

        trainingListFragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
            if (viewLifecycleOwner != null) {
                Navigation.setViewNavController(trainingListFragment.requireView(), navController)
            }
        }

        `when`(viewModel.trainingsLiveData).thenReturn(trainingsLiveData)
        `when`(viewModel.isPremium).thenReturn(isPremiumLiveData)
        `when`(viewModel.trainingDeleted).thenReturn(trainingDeletedLiveData)
        `when`(viewModel.navigateToCreateTraining).thenReturn(navigateToCreateTrainingLiveData)
        `when`(viewModel.navigateToExerciseList).thenReturn(navigateToExerciseListLiveData)

        val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
            trainingListFragment
        }
        dataBindingIdlingResourceRule.monitorFragment(scenario)
        scenario.onFragment { fragment ->
            Navigation.setViewNavController(fragment.requireView(), navController)
            fragment.binding.trainingListRv.itemAnimator = null
            fragment.disableProgressBarAnimations()
        }
    }

}

Вы, ребята, знаете, почему это происходит и как я могу это исправить? Я действительно борюсь с этим:(

1 ответ

Решение

Проблема, с которой вы столкнулись, связана с переменным затенением.

Когда вы пишете этот код

viewModel = mock(TrainingListViewModel::class.java)

val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
    TrainingListFragment().apply {
        appExecutors = countingAppExecutors.appExecutors
        viewModelFactory = ViewModelUtil.createFor(viewModel)
    }
}

В ViewModelUtil.createFor(viewModel)это не с помощьюviewModel переменная, которую вы создали с помощью mock выше, но вместо этого используется viewModel переменная ** в вашем TrainingListFragment. Это потому, что вы используетеapply { }, что означает, что область действия, к которой применяется код, - это сам фрагмент - это эффективно, как если бы ваш код находился внутри самого этого фрагмента, поэтому viewModel действительно TrainingListViewModel.this.viewModel.

Вот почему ваше сообщение об ошибке говорит

at com.maximesarrato.lafayapp.ui.training.TrainingListFragment.getViewModel(Unknown Source:7)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragmentTest$init$$inlined$launchFragmentInContainer$1.instantiate(FragmentScenario.kt:114)

Ты звонишь getViewModel() т.е. доступ к viewModel собственность изнутри вашего instantiate блок.

Самое простое решение - просто использовать другое имя переменной для вашей имитации viewModel вместо того же имени viewModel. Если бы это было названоmockViewModel, затем он звонит ViewModelUtil.createFor(mockViewModel) будет правильно ссылаться на вашу макет ViewModel, поскольку в вашем фрагменте не будет никакой переменной с тем же именем.

Другой вариант - использовать also вместо того apply:

viewModel = mock(TrainingListViewModel::class.java)

val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
    TrainingListFragment().also { fragment ->
        fragment.appExecutors = countingAppExecutors.appExecutors
        fragment.viewModelFactory = ViewModelUtil.createFor(viewModel)
    }
}

also, нравиться apply, всегда возвращает исходный объект - ваш TrainingListFragment (это правильно и что вы хотите вернуть).

Однако при использовании also вам нужно назвать переменную, с которой вы работаете (здесь я использую fragment а не по умолчанию it) и вам нужно специально использовать fragment.viewModelесли вы хотите сослаться на что-то внутри переменной фрагмента. Опять же, без затенения имени, в данном случаеviewModel указал бы на единственный viewModel переменная в области видимости - ваш макет ViewModel.

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