Search code examples
androidandroid-fragmentsandroid-testingrobolectricdagger-hilt

Hilt Fragment has to be attached to @AndroidEntryPoint Activity, while testing (it is attached to @AndEntPoint marked activity)


I followed a lot of tutorials/ articles and the googles architecture sample

No matter what I try I keep getting an error saying that I need to attach Fragment to @AndroidEntryPoint annotated Activity. I've setup everything correctly but still can't get this to work properly.

java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.nikolam.colorme.HiltTestActivity
    at dagger.hilt.internal.Preconditions.checkState(Preconditions.java:83)
    at dagger.hilt.android.internal.managers.FragmentComponentManager.createComponent(FragmentComponentManager.java:75)
    at dagger.hilt.android.internal.managers.FragmentComponentManager.generatedComponent(FragmentComponentManager.java:64)
    at com.nikolam.main_feature.presenter.main_screen.Hilt_MainFragment.generatedComponent(Hilt_MainFragment.java:80)
    at com.nikolam.main_feature.presenter.main_screen.Hilt_MainFragment.inject(Hilt_MainFragment.java:102)
    at com.nikolam.main_feature.presenter.main_screen.Hilt_MainFragment.initializeComponentContext(Hilt_MainFragment.java:63)
    at com.nikolam.main_feature.presenter.main_screen.Hilt_MainFragment.onAttach(Hilt_MainFragment.java:55)
    at androidx.fragment.app.Fragment.onAttach(Fragment.java:1783)
    at com.nikolam.main_feature.presenter.main_screen.Hilt_MainFragment.onAttach(Hilt_MainFragment.java:45)
    at androidx.fragment.app.Fragment.performAttach(Fragment.java:2911)
    at androidx.fragment.app.FragmentStateManager.attach(FragmentStateManager.java:464)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:275)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
    at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1971)
    at androidx.fragment.app.BackStackRecord.commitNow(BackStackRecord.java:305)
    at com.nikolam.colorme.main_feature.MainFragmentTest$whenMainActivityLaunchedNavigatorIsInvokedForFragment$$inlined$launchFragmentInHiltContainer$1.perform(HiltExt.kt:46)
    at androidx.test.core.app.ActivityScenario.lambda$onActivity$2$ActivityScenario(ActivityScenario.java:660)
    at androidx.test.core.app.ActivityScenario$$Lambda$4.run(Unknown Source)
    at androidx.test.core.app.ActivityScenario.onActivity(ActivityScenario.java:670)
    at com.nikolam.colorme.main_feature.MainFragmentTest.whenMainActivityLaunchedNavigatorIsInvokedForFragment(MainFragmentTest.kt:85)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at dagger.hilt.android.internal.testing.MarkThatRulesRanRule$1.evaluate(MarkThatRulesRanRule.java:106)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:575)
    at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
    at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

Now here is my setup.

Dependencies

    testImplementation(TestLibraryDependency.HILT_ANDROID_TESTING)
    kaptTest(TestLibraryDependency.HILT_ANDROID_TESTING_COMPILER)
    testImplementation(TestLibraryDependency.TEST_RUNNER)
    testImplementation(TestLibraryDependency.ESPRESSO_CORE)
    testImplementation(TestLibraryDependency.ANDROIDX_TEST_RULES)
    testImplementation(TestLibraryDependency.ANDROIDX_CORE_TESTING)
    testImplementation(TestLibraryDependency.ANDROIDX_TEST_EXT)

This is my test

@HiltAndroidTest
@Config(application = HiltTestApplication::class, maxSdk = Build.VERSION_CODES.P)
@RunWith(RobolectricTestRunner::class)
class MainFragmentTest {

    @get:Rule()
    var hiltAndroidRule = HiltAndroidRule(this)

    @Before
    fun init() {
        hiltAndroidRule.inject()
    }

    @Test
    fun whenMainActivityLaunchedNavigatorIsInvokedForFragment() {
     //   launchActivity()
        // GIVEN - On the home screen
        val navController = mock(NavController::class.java)

        var fragment = launchFragmentInHiltContainer<MainFragment>() {
            Navigation.setViewNavController(this.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_floating_action)).perform(ViewActions.click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            Uri.parse(UPLOAD_DEEPLINK)
        )
    }

Launch in hilt container per google

inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
    crossinline action: Fragment.() -> Unit = {}
) {
    val startActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            HiltTestActivity::class.java
        )
    ).putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId)

    ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
        val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )
        fragment.arguments = fragmentArgs
        activity.supportFragmentManager
            .beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        fragment.action()
    }
}

(I also have their custom runner)

I also have a debug source set that contains HiltTestActivity like here, alongside debug manifest.

Besides this, I'd like to somehow inject and initialize my NavigationManager (wrapper class around navController) in my Activity. Is there a way to achieve this? When I tried to use my MainActivity for tests I kept getting errors because my lateinit @Injects weren't getting initialized...

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding;

    private lateinit var navController : NavController
    @Inject
    lateinit var navManager: NavManager

    private fun initNavManager() {
        navManager.setOnNavEvent {
            navController.navigate(it)
        }

Solution

  • In my case I had to use

        hilt {
            enableTransformForLocalTests = true
        }
    

    Since the Roboelectric/ Hilt has 20 bugs currently being tracked it was hard to pinpoint what workaround was the case :)