google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.35k stars 2k forks source link

Exception when using Hilt injection in test class when using androidx.startup Initializer injection #3906

Closed newmanw closed 1 year ago

newmanw commented 1 year ago

Currently following the testing guide here: https://developer.android.com/training/dependency-injection/hilt-testing

Injection in androidx.startup implementation from https://proandroiddev.com/app-startup-hilt-7d253d60772f

Testing this composable with hilt injected viewmodel

@Composable
fun MyScreen(
   viewModel: MyViewModel = hiltViewModel()
) {
}

Test class:

@HiltAndroidTest
class MyComposeTest {

   @get:Rule(order = 0)
   var hiltRule = HiltAndroidRule(this)

   @get:Rule(order = 1)
   val composeRule = createComposeRule()

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

   @Test
   fun canary() {
    // This test fails with exception
   }
}

Custom test runner:

class CustomTestRunner : AndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

build.gradle:

android {
    defaultConfig {
        testInstrumentationRunner "com.example.android.dagger.CustomTestRunner"
    }
}

Helper to use hilt injection in androix.startup Initializer

@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppInitializer {

   companion object {
      // Resolve the InitializerEntryPoint from a context
      fun resolve(context: Context): AppInitializer {
         val appContext = context.applicationContext ?: throw IllegalStateException()
         return EntryPointAccessors.fromApplication(
            appContext,
            AppInitializer::class.java
         )
      }
   }

   fun inject(workManagerInitializer: WorkManagerInitializer)
   fun inject(otherInitializer: OtherInitializer)
} 

Exception:

java.lang.RuntimeException: Unable to get provider androidx.startup.InitializationProvider: androidx.startup.StartupException: androidx.startup.StartupException: java.lang.IllegalStateException: The component was not created. Check that you have added the HiltAndroidRule.
...
Caused by: java.lang.IllegalStateException: The component was not created. Check that you have added the HiltAndroidRule.
    at dagger.hilt.internal.Preconditions.checkState(Preconditions.java:83)
    at dagger.hilt.android.internal.testing.TestApplicationComponentManager.generatedComponent(TestApplicationComponentManager.java:96)
    at dagger.hilt.android.testing.HiltTestApplication.generatedComponent(HiltTestApplication.java:50)
    at dagger.hilt.EntryPoints.get(EntryPoints.java:59)
    at dagger.hilt.android.EntryPointAccessors.fromApplication(EntryPointAccessors.kt:35)
    at com.test.AppInitializer$Companion.resolve(AppInitializer.kt:27)

Exception is throw in call to EntryPointAccessors.fromApplication(...)

I have verified with logging that CustomTestRunner.newApplication(...) is called. However it looks like AppInitializer.resolve() is called before the the tests @Before setup method.

Possible to use hilt injection in tests with my setup to inject into androidx.startup Initializers?

bcorso commented 1 year ago

@newmanw you can check out https://dagger.dev/hilt/early-entry-point.

That should cover your use case, but be sure to also read the caveats.

newmanw commented 1 year ago

@bcorso thank you so much for the quick response. Unfortunately I am seeing the same thing when using early entry point.

Example:

@EarlyEntryPoint
@InstallIn(SingletonComponent::class)
interface AppInitializer {

   companion object {
      // Resolve the InitializerEntryPoint from a context
      fun resolve(context: Context): AppInitializer {
         val appContext = context.applicationContext ?: throw IllegalStateException()
         return EntryPointAccessors.fromApplication(
            appContext,
            AppInitializer::class.java
         )
      }
   }

   fun inject(workManagerInitializer: WorkManagerInitializer)
   fun inject(otherInitializer: OtherInitializer)
}

Possible that I need to update access using EntryPointAccessors.fromApplication(...)? Something like EarlyEntryPointAccessors?

bcorso commented 1 year ago

Possible that I need to update access using EntryPointAccessors.fromApplication(...)? Something like EarlyEntryPointAccessors?

Yes, I don't think we have an EarlyEntryPointAccessors but you can use:

EarlyEntryPoints.get(appContext, AppInitializer::class.java)
newmanw commented 1 year ago

@bcorso great thanks!

Assume that I have everything setup correctly hilt will inject the viewmodel into the composable? I am currently seeing

java.lang.RuntimeException: Cannot create an instance of class com.example.android.dagger.MyViewModel

Chang-Eric commented 1 year ago

That exception usually happens when your ViewModel is being created with the default ViewModel factory and not the Hilt one. Since you're using createComposeRule(), it probably is just creating a default non-Hilt activity for you. You probably want to use the version that lets you specify the activity and pass it an @AndroidEntryPoint activity.

newmanw commented 1 year ago

@Chang-Eric thank you, however not sure I fully understand.

I have tried:

@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()

Which now just leads to other problems. I have read numerous threads, including; https://github.com/google/dagger/issues/2033, which just confuse the issue more for me with different exceptions popping up. Compose testing is well documented as is hilt testing, however there seems to be a big gab in hilt and compose testing when it comes to injected view models using hiltViewModel().

Chang-Eric commented 1 year ago

Which now just leads to other problems.

It is hard to say without knowing what those other problems are, but it sounds like it maybe could have fixed the ViewModel issue you were seeing then? While I understand we don't have documentation specifically for testing with Compose, it is a bit hard for us to do documentation for every combination of library you might be using. So far, I am not seeing anything here that is a specific problem with Compose and Hilt.

I think the original issue you were seeing has been addressed, as has (maybe?) the ViewModel issue if you still aren't seeing that. For further problems, it might be easier getting help from places like StackOverflow and then circle back here if there's something wrong that needs to be fixed with using Hilt and Compose together.

Nniheke commented 11 months ago

check that your path is properly defined com.example.android.dagger.MyViewModel if not update in your build gradle the actual path