Hilt: Example instrumentation test using navGraphViewModels, FragmentScenario and launchFragmentInHiltContainer
See original GitHub issueHello,
I’m trying to write an instrumentation test using:
- navigation graph scoped viewmodel
- FragmentScenario api
- launchFragmentInHiltContainer (referenced https://developer.android.com/training/dependency-injection/hilt-testing here)
- databinding
Here is some example test code:
@HiltAndroidTest
class ForgotPasswordFragmentTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Test
fun testEmptyEmailDisablesSendButton() {
val testNavHostController = TestNavHostController(ApplicationProvider.getApplicationContext())
testNavHostController.setViewModelStore(ViewModelStore())
testNavHostController.setGraph(R.navigation.navigation_graph)
testNavHostController.setCurrentDestination(R.id.forgotPassword)
launchFragmentInHiltContainer<ForgotPasswordFragment> {
ForgotPasswordFragment().also { fragment ->
// In addition to returning a new instance of our Fragment,
// get a callback whenever the fragment’s view is created
// or destroyed so that we can set the NavController
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) { // The fragment’s view has just been created
Navigation.setViewNavController(fragment.requireView(), testNavHostController)
onView(withId(R.id.retrievePasswordButton)).check(matches(isNotEnabled()))
}
}
}
}
This however always crashes with IllegalStateException: View androidx.constraintlayout.widget.ConstraintLayout{39c0d6e V.E… …I. 0,0-0,0} does not have a NavController set.
Looking at the stacktrace it happens when in onViewCreated() I do:
viewBinding.viewModel = onboardingViewModel // pass in the onboarding view model
It tries to lazily get the onboardingViewModel which is intialized as:
private val onboardingViewModel: OnboardingViewModel by navGraphViewModels(R.id.onboarding_graph) { defaultViewModelProviderFactory }
Caused by: java.lang.IllegalStateException: View androidx.constraintlayout.widget.ConstraintLayout{39c0d6e V.E...... ......I. 0,0-0,0} does not have a NavController set
at androidx.navigation.Navigation.findNavController(Navigation.java:84)
at androidx.navigation.fragment.NavHostFragment.findNavController(NavHostFragment.java:118)
at androidx.navigation.fragment.FragmentKt.findNavController(Fragment.kt:29)
at be.joyn.business.onboarding.ForgotPasswordFragment$$special$$inlined$navGraphViewModels$1.invoke(NavGraphViewModelLazy.kt:56)
at be.joyn.business.onboarding.ForgotPasswordFragment$$special$$inlined$navGraphViewModels$1.invoke(Unknown Source:0)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at be.joyn.business.onboarding.ForgotPasswordFragment$$special$$inlined$navGraphViewModels$2.invoke(NavGraphViewModelLazy.kt:59)
at be.joyn.business.onboarding.ForgotPasswordFragment$$special$$inlined$navGraphViewModels$2.invoke(Unknown Source:0)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
at be.joyn.business.onboarding.ForgotPasswordFragment.getOnboardingViewModel(Unknown Source:2)
at be.joyn.business.onboarding.ForgotPasswordFragment.onViewCreated(ForgotPasswordFragment.kt:48)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:332)
I thought maybe it’s due to databinding not having al its work done (https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-testing-survey/#7) I tried using DataBindingIdlingResource.kt but monitorFragment needs a FragmentScenario as parameter which I don’t have because launchFragmentInHiltContainer returns Unit
It would be really helpfull to have an example using the above technologies in https://github.com/android/architecture-samples/tree/dev-hilt
thanks
Issue Analytics
- State:
- Created 3 years ago
- Comments:7
Top GitHub Comments
@ashley-figueira
I’m currently using:
`inline fun <reified T : Fragment> launchFragmentInHiltContainer( fragmentArgs: Bundle? = null, @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, navHostController: NavHostController? = null, fragmentFactory: FragmentFactory? = null, crossinline action: T.() -> Unit = {}
): ActivityScenario<HiltTestActivity> {
}`
I’m using a Junit4 rule to pass in a TestNavHostController:
` class TestNavHostControllerRule( @NavigationRes private val navigationGraph: Int, @IdRes private val currentDestination: Int, private val viewModelStore: ViewModelStore = ViewModelStore() ) : ExternalResource() {
}
// This class is used as an empty container activity that is Hilt compatible for use with fragment scenario tests @AndroidEntryPoint class HiltTestActivity : AppCompatActivity()`
val activityScenario = launchFragmentInHiltContainer<ForgotPasswordFragment>( navHostController = testNavHostControllerRule.testNavHostController )
Using this way allows me to launch the fragment without exceptions even when having the following construct in my fragment:
private val onboardingViewModel: OnboardingViewModel by navGraphViewModels( R.id.onboarding_graph ) { defaultViewModelProviderFactory }
ps: I’m using Databinding so I also need an Espresso Idling resource too
@ashley-figueira the code from @wbervoets does not work for me either. At the point which the fragment is added to the activity
the
testNavHostController
is still not set meaning the setting up of my toolbar fails. This is because theobserveForever
lambda expression is never called. Any idea why?EDIT: Okay the problem here was where I was setting up my toolbar. I realise now that it needs to be done
fun onViewCreated(view: View, savedInstanceState: Bundle?)
since the observer will receive events when the onCreateView has been executed