question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hilt: Example instrumentation test using navGraphViewModels, FragmentScenario and launchFragmentInHiltContainer

See original GitHub issue

Hello,

I’m trying to write an instrumentation test using:

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:closed
  • Created 3 years ago
  • Comments:7

github_iconTop GitHub Comments

3reactions
wbervoetscommented, Nov 6, 2020

@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> {

val startActivityIntent = Intent.makeMainActivity(
    ComponentName(
        ApplicationProvider.getApplicationContext(),
        HiltTestActivity::class.java
    )
).putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId)

return ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
    fragmentFactory?.let {
        activity.supportFragmentManager.fragmentFactory = it
    }
    val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
        Preconditions.checkNotNull(T::class.java.classLoader),
        T::class.java.name
    )

    fragment.arguments = fragmentArgs

    navHostController?.let {
        fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
            if (viewLifecycleOwner != null) {
                Navigation.setViewNavController(fragment.requireView(), it)
            }
        }
    }

    activity.supportFragmentManager
        .beginTransaction()
        .add(android.R.id.content, fragment, "")
        .commitNow()

    (fragment as T).action()
}

}`

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() {

lateinit var testNavHostController: TestNavHostController
    private set

override fun before() {
    super.before()
    // Before test
    testNavHostController = TestNavHostController(ApplicationProvider.getApplicationContext())
    testNavHostController.setViewModelStore(viewModelStore)
    testNavHostController.setGraph(navigationGraph)
    testNavHostController.setCurrentDestination(currentDestination)
}

} // 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

1reaction
goldy1992commented, Nov 12, 2020

@ashley-figueira the code from @wbervoets does not work for me either. At the point which the fragment is added to the activity

 activity.supportFragmentManager
                .beginTransaction()
                .add(android.R.id.content, fragment, "")
                .commitNow()

the testNavHostController is still not set meaning the setting up of my toolbar fails. This is because the observeForever 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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Hilt testing guide | Android Developers
To use the Hilt test application in instrumented tests, you need to configure a new test runner. This makes Hilt work for all...
Read more >
Testing Fragments with Dagger-Hilt - Part 12 - YouTube
In this video I will show you a workaround to be able to test fragments with the help of Dagger- Hilt.⭐ Get certificates...
Read more >
Testing With Hilt Tutorial: UI and Instrumentation Tests
Learn how to get started with testing with Hilt by writing UI and instrumentation tests.
Read more >
To Make Fragment Test Under Hilt Installed | by Homan Huang
I have tried on the Hilt-Fragment test method for a while. I found that is not hard to get around. — === Menu...
Read more >
Simplifying Jetpack Navigation between top-level destinations ...
We must also add the @InstallIn(SomeComponent::class) annotation on our modules. Dagger-Hilt and ViewModel injection. Hilt allows injecting ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found