I'm using almost the as same architecture as Google sample: GithubBrowserSample.
This field is injected in my Fragment class:
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel: TrainingListViewModel by viewModels {
viewModelFactory
}
In all my fragments tests I got this error which corresponds to a field that is instantiated by injection:
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)
This happens when I set my fragment like this with 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()
}
But when I setup the Fragment before using it with FragmentScenario it works:
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
}
Here is the source code of my test:
@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()
}
}
}
Do you guys have an idea about why this is happening and how I can fix it? I'm really struggling on this :(
The problem you're experiencing is due to variable shadowing.
When you write this code
viewModel = mock(TrainingListViewModel::class.java)
val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
TrainingListFragment().apply {
appExecutors = countingAppExecutors.appExecutors
viewModelFactory = ViewModelUtil.createFor(viewModel)
}
}
The ViewModelUtil.createFor(viewModel)
is not using the viewModel
variable that you've created with mock
above, but is instead using the viewModel
variable **within your TrainingListFragment
. This is because you are using apply { }
, which means that the scope that the code applies to is the fragment itself - its effectively as if your code was within that fragment itself so viewModel
is really TrainingListViewModel.this.viewModel
.
This is why your error message says
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)
You're calling getViewModel()
i.e.., accessing the viewModel
property from within your instantiate
block.
The easiest solution is to just use a different variable name for your mock viewModel instead of using the same name of viewModel
. If it was named mockViewModel
, then it calling ViewModelUtil.createFor(mockViewModel)
would correctly reference your mock ViewModel since there wouldn't be any variable from your fragment with that same name.
The other option is to use also
instead of 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
, like apply
, always returns the original object - your TrainingListFragment
(this is correct and what you want to return).
However, when using also
you have to name the variable you're operating on (here I use fragment
rather than the default it
) and you need to specifically use fragment.viewModel
if you wanted to refer to something within the fragment variable. Again, without any name shadowing, in this case viewModel
would point to the only viewModel
variable in scope - your mock ViewModel.