Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hilt can only successfully inject dependencies in instrumented tests if all the associated modules are marked with SingletonComponent and dependencies marked with Singleton. Is this how it is supposed to work? #3695

Closed
sarimmehdi opened this issue Jan 8, 2023 · 0 comments

Comments

@sarimmehdi
Copy link

sarimmehdi commented Jan 8, 2023

Here's an example:

@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashScreenFragment: Fragment() {

    @Inject
    lateinit var splashTimer: SplashTimer

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.splash_screen, container, false)

        splashTimer.start(
            timeOfOnePeriodMs = SplashScreenViewModelImpl.SPLASH_PERIODIC_DELAY_MS,
            totalTimeMs = SplashScreenViewModelImpl.SPLASH_SCREEN_TIME_MS,
            onIntervalElapsed = {},
            onTimerElapsed = { println("splash timer expired") }
        )
        return view
    }
}

And here are the modules associated to it:

@Module
@InstallIn(FragmentComponent::class)
abstract class CoreUiModule {

    @Binds
    @FragmentScoped
    abstract fun bindSplashTimer(
        actualSplashTimer: ActualSplashTimer
    ): SplashTimer
}

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [CoreUiModule::class]
)
abstract class TestCoreUiModule {

    @Binds
    @Singleton
    abstract fun bindSplashTimer(
        testSplashTimer: TestSplashTimer
    ): SplashTimer
}

The above setup works fine when running the app and also when running the following instrumented test:

@HiltAndroidTest
class SplashFragmentTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var splashTimer: SplashTimer

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun splashScreenExitsToLoginScreenAfter3Seconds() {
        launchFragmentInHiltContainer<SplashScreenFragment> {

        }
    }
}

But if I replace TestCoreUiModule abstract class with the following (adding the same annotations as were added to CoreUiModule):

@Module
@TestInstallIn(
    components = [FragmentComponent::class],
    replaces = [CoreUiModule::class]
)
abstract class TestCoreUiModule {

    @Binds
    @FragmentScoped
    abstract fun bindSplashTimer(
        testSplashTimer: TestSplashTimer
    ): SplashTimer
}

The test doesn't run and instead I get the following error:

/home/sarimmehdi/android_project/feature_splash/splash_presentation/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/Default_HiltComponents.java:146: error: [Dagger/MissingBinding] com.project.coreui.splashtimer.SplashTimer cannot be provided without an @Provides-annotated method.
  public abstract static class SingletonC implements SplashFragmentTest_GeneratedInjector,
                         ^
  A binding for com.project.coreui.splashtimer.SplashTimer exists in dagger.hilt.android.internal.testing.root.Default_HiltComponents.FragmentC:
      com.project.coreui.splashtimer.SplashTimer is injected at
          [dagger.hilt.android.internal.testing.root.Default_HiltComponents.SingletonC] com.project.splash_presentation.fragment.SplashFragmentTest.splashTimer
      com.project.splash_presentation.fragment.SplashFragmentTest is injected at
          [dagger.hilt.android.internal.testing.root.Default_HiltComponents.SingletonC] com.project.splash_presentation.fragment.SplashFragmentTest_GeneratedInjector.injectTest(com.project.splash_presentation.fragment.SplashFragmentTest)

So, does that mean hilt only injects dependencies marked as singletons in instrumented tests? If true, how do I inject my AbstractSavedStateViewModelFactory?

@Module
@InstallIn(FragmentComponent::class)
object SplashPresentationModule {

    @Provides
    @FragmentScoped
    fun provideFactory(
        fragment: Fragment,
        splashScreenUseCases: SplashScreenUseCases
    ): AbstractSavedStateViewModelFactory {
        return RealSplashScreenViewModelFactory(fragment, fragment.arguments, splashScreenUseCases)
    }
}

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [SplashPresentationModule::class]
)
object TestSplashPresentationModule {

    @Provides
    @Singleton
    fun provideFactory(
        fragment: Fragment,
        splashScreenUseCases: SplashScreenUseCases
    ): AbstractSavedStateViewModelFactory {
        return TestSplashScreenViewModelFactory(fragment, fragment.arguments, splashScreenUseCases)
    }
}

Again, the above works fine when I am simply running the app but not when I try to run the instrumented test. One thing to note is that I took the code for injecting viewmodels from here. Over there, hilt injection seems to work fine even though the test dependencies are not marked as singletons. However, he is defining the test module inside the androidTest folder whereas I am defining all dependencies inside the main folder.

Just to add more context, here is the new error message I get related to trying to inject the test ViewModel factory:

/home/sarimmehdi/android_project/feature_splash/splash_presentation/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/Default_HiltComponents.java:147: error: [Dagger/MissingBinding] androidx.fragment.app.Fragment cannot be provided without an @Inject constructor or an @Provides-annotated method.
  public abstract static class SingletonC implements SplashFragmentTest_GeneratedInjector,
                         ^
      androidx.fragment.app.Fragment is injected at
          com.project.splash_presentation.di.TestSplashPresentationModule.provideFactory(fragment, …)
      androidx.lifecycle.AbstractSavedStateViewModelFactory is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest.factory
      com.project.splash_presentation.fragment.SplashFragmentTest is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest_GeneratedInjector.injectTest(com.project.splash_presentation.fragment.SplashFragmentTest)
  The following other entry points also depend on it:
      com.project.splash_presentation.fragment.SplashScreenFragment_GeneratedInjector.injectSplashScreenFragment(com.project.splash_presentation.fragment.SplashScreenFragment) [dagger.hilt.android.internal.testing.root.Default_HiltComponents.SingletonC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityRetainedC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.FragmentC]
/home/sarimmehdi/android_project/feature_splash/splash_presentation/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/Default_HiltComponents.java:147: error: [Dagger/MissingBinding] com.project.login_domain.repository.UserRepository cannot be provided without an @Provides-annotated method.
  public abstract static class SingletonC implements SplashFragmentTest_GeneratedInjector,
                         ^
      com.project.login_domain.repository.UserRepository is injected at
          com.project.splash_domain.di.TestSplashDomainModule.provideSplashScreenUseCases(userRepository, …)
      com.project.splash_domain.usecase.screen.SplashScreenUseCases is injected at
          com.project.splash_presentation.di.TestSplashPresentationModule.provideFactory(…, splashScreenUseCases)
      androidx.lifecycle.AbstractSavedStateViewModelFactory is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest.factory
      com.project.splash_presentation.fragment.SplashFragmentTest is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest_GeneratedInjector.injectTest(com.project.splash_presentation.fragment.SplashFragmentTest)
  The following other entry points also depend on it:
      com.project.splash_presentation.fragment.SplashScreenFragment_GeneratedInjector.injectSplashScreenFragment(com.project.splash_presentation.fragment.SplashScreenFragment) [dagger.hilt.android.internal.testing.root.Default_HiltComponents.SingletonC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityRetainedC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.FragmentC]
/home/sarimmehdi/android_project/feature_splash/splash_presentation/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/Default_HiltComponents.java:147: error: [Dagger/MissingBinding] com.project.onboarding_domain.repository.OnboardingRepository cannot be provided without an @Provides-annotated method.
  public abstract static class SingletonC implements SplashFragmentTest_GeneratedInjector,
                         ^
      com.project.onboarding_domain.repository.OnboardingRepository is injected at
          com.project.splash_domain.di.TestSplashDomainModule.provideSplashScreenUseCases(…, onboardingRepository)
      com.project.splash_domain.usecase.screen.SplashScreenUseCases is injected at
          com.project.splash_presentation.di.TestSplashPresentationModule.provideFactory(…, splashScreenUseCases)
      androidx.lifecycle.AbstractSavedStateViewModelFactory is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest.factory
      com.project.splash_presentation.fragment.SplashFragmentTest is injected at
          com.project.splash_presentation.fragment.SplashFragmentTest_GeneratedInjector.injectTest(com.project.splash_presentation.fragment.SplashFragmentTest)
  The following other entry points also depend on it:
      com.project.splash_presentation.fragment.SplashScreenFragment_GeneratedInjector.injectSplashScreenFragment(com.project.splash_presentation.fragment.SplashScreenFragment) [dagger.hilt.android.internal.testing.root.Default_HiltComponents.SingletonC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityRetainedC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.ActivityC → dagger.hilt.android.internal.testing.root.Default_HiltComponents.FragmentC]
4 errors

The reason why I think it has to do with annotations is because if I remove ViewModel from the dependency graph, the tests run fine and I am able to inject everything else. Everything seems to fall apart the moment viewmodels get involved as hilt seems to inject them within the scope of Fragments and not Singletons.

@sarimmehdi sarimmehdi changed the title Hilt can only successfully inject dependencies in an instrumented tests if all the associated modules are marked with SingletonComponent and dependencies marked with Singleton. Is this how it is supposed to work? Hilt can only successfully inject dependencies in instrumented tests if all the associated modules are marked with SingletonComponent and dependencies marked with Singleton. Is this how it is supposed to work? Jan 8, 2023
copybara-service bot pushed a commit that referenced this issue Jan 10, 2023
When looking at a recent issue (#3695) I realized we didn't actually have test coverage for `@TestInstallIn` for components other than `SingletonComponent`. This CL adds tests for `ActivityComponent` and `FragmentComponent`.

RELNOTES=N/A
PiperOrigin-RevId: 500828531
copybara-service bot pushed a commit that referenced this issue Jan 10, 2023
When looking at a recent issue (#3695) I realized we didn't actually have test coverage for `@TestInstallIn` for components other than `SingletonComponent`. This CL adds tests for `ActivityComponent` and `FragmentComponent`.

RELNOTES=N/A
PiperOrigin-RevId: 500828531
copybara-service bot pushed a commit that referenced this issue Jan 10, 2023
When looking at a recent issue (#3695) I realized we didn't actually have test coverage for `@TestInstallIn` for components other than `SingletonComponent`. This CL adds tests for `ActivityComponent` and `FragmentComponent`.

RELNOTES=N/A
PiperOrigin-RevId: 500828531
copybara-service bot pushed a commit that referenced this issue Jan 10, 2023
When looking at a recent issue (#3695) I realized we didn't actually have test coverage for `@TestInstallIn` for components other than `SingletonComponent`. This CL adds tests for `ActivityComponent` and `FragmentComponent`.

RELNOTES=N/A
PiperOrigin-RevId: 501018396
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant