google / dagger

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

please check this code. as I am trying to add the repository with @BindValue. the hilt is throwing error as follows ( issue found in hilt version 2.14) #3360

Closed sheiapp closed 2 years ago

sheiapp commented 2 years ago

please check this code. as I am trying to add the repository with @BindValue. the hilt is throwing error as follows.


@HiltAndroidTest
@UninstallModules(AppModule::class)
@RunWith(AndroidJUnit4::class)
class RandomUserListFragmentTest {
    @get:Rule
    val hiltRule = HiltAndroidRule(this)

 @Inject
    lateinit var glideRequestManager: RequestManager

    @BindValue
    @JvmField  val repository: RandomUserRepositoryImplTest = mock()

  @Before
    fun launchFragment() {
        hiltRule.inject()
        ActivityScenario.launch(RandomUserActivity::class.java)
    }

   @Test
    fun verifyTheRecyclerViewHasData(){
        Espresso.onView(ViewMatchers.withId(R.id.recyclerView)).check { view, noViewFoundException ->
            if (noViewFoundException != null) {
                throw noViewFoundException
            }

            val recyclerView = view as RecyclerView
            Assert.assertEquals(3, recyclerView.adapter?.itemCount)
        }
    }
}
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
object FakeAppModuleTest {
    @Provides
    @Singleton
    fun initGlide(@ApplicationContext appContext: Context): RequestManager = Glide.with(appContext)
        .setDefaultRequestOptions(
            RequestOptions()
                .centerInside()
                .error(R.drawable.ic_user)
        )
}

getting errors like

Task :app:hiltJavaCompileDebugAndroidTest /Users/shaheer/AndroidStudioProjects/ssassignment/app/build/generated/hilt/component_sources/debugAndroidTest/com/ss/assignment/ui/random_user_ui/RandomUserActivityTest_TestComponentDataSupplier.java:8: error: cannot find symbol import dagger.hilt.android.internal.testing.root.DaggerRandomUserActivityTest_HiltComponents_SingletonC; ^ symbol: class DaggerRandomUserActivityTest_HiltComponents_SingletonC location: package dagger.hilt.android.internal.testing.root /Users/shaheer/AndroidStudioProjects/ssassignment/app/build/generated/hilt/component_sources/debugAndroidTest/com/ss/assignment/ui/random_user_ui/main_screen/RandomUserListFragmentTest_TestComponentDataSupplier.java:8: error: cannot find symbol import dagger.hilt.android.internal.testing.root.DaggerRandomUserListFragmentTest_HiltComponents_SingletonC; ^ symbol: class DaggerRandomUserListFragmentTest_HiltComponents_SingletonC location: package dagger.hilt.android.internal.testing.root /Users/shaheer/AndroidStudioProjects/ssassignment/app/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/RandomUserActivityTest_HiltComponents.java:133: error: [Dagger/MissingBinding] com.ss.assignment.data.repository.RandomUserRepository cannot be provided without an @Provides-annotated method. public abstract static class SingletonC implements SunAndSandsApp_GeneratedInjector, ^ com.ss.assignment.data.repository.RandomUserRepository is injected at com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel(randomUserRepository) com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel is injected at com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel_HiltModules.BindsModule.binds(arg0) @dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.String,javax.inject.Provider> is requested at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.SingletonC → dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.ActivityRetainedC → dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.ViewModelC] 3 errors

Hilt version 2.14

bcorso commented 2 years ago

The error message says you're missing a binding for RandomUserRepository. Do you provide one?

sheiapp commented 2 years ago

Hi @bcorso

RandomUserRepositoryImplTest - This class is an fake implementations of RandomUserRepository interface.

my idea to mock this fake object in the test class. So that i can mock the its behavior accordingly.

Thank you

bcorso commented 2 years ago

Dagger does not know that RandomUserRepositoryImplTest is an implementation of RandomUserRepository unless you tell it. Rather than using @BindValue you should just use a normal nested module:

@HiltAndroidTest
@UninstallModules(AppModule::class)
@RunWith(AndroidJUnit4::class)
class RandomUserListFragmentTest {
  @Module
  @InstallIn(SingletonComponent::class)
  interface TestModule {
    // Bind the implementation to the interface.
    @Binds fun bind(impl: RandomUserRepositoryImplTest): RandomUserRepository

    companion object {
      @Provides
      @Singleton
      fun provideImpl(): RandomUserRepositoryImplTest = mock()
    }
  }

  @Inject lateinit var glideRequestManager: RequestManager
  @Inject lateinit var mockRandomUserRepository: RandomUserRepositoryImplTest

  // ...
}

Edit: Added @Singleton to the mock binding, as that will likely need to be scoped if you do any sort of setup with the mock instance in your test.

sheiapp commented 2 years ago

Hi @bcorso

I tried with this code snippet

@HiltAndroidTest
@UninstallModules(AppModule::class)
@RunWith(AndroidJUnit4::class)
class RandomUserListFragmentTest {
    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var glideRequestManager: RequestManager

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

    @Module
    @InstallIn(SingletonComponent::class)
    interface TestModule {

        @Binds fun bind(impl: RandomUserRepositoryImplTest): RandomUserRepository

        companion object {
            @Provides
            @Singleton
            fun provideImpl(): RandomUserRepositoryImplTest = mock()
        }
    }

    @Inject
    lateinit var repository: RandomUserRepositoryImplTest

    @Test
    fun testTheLoadingStateOfTheFragmentAfterGettingTheData() {
        whenever(repository.getRandomUserList())
            .doReturn(flowOf(PagingData.from(emptyList())))
        ActivityScenario.launch(RandomUserActivity::class.java)
        Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
            .check { view, noViewFoundException ->
                if (noViewFoundException != null) {
                    throw noViewFoundException
                }

                val recyclerView = view as RecyclerView
                Assert.assertEquals(0, recyclerView.adapter?.itemCount)
            }
    }

still Iam getting error like /Users/shaheer/AndroidStudioProjects/ss/app/build/generated/hilt/component_sources/debugAndroidTest/com/ss/assignment/ui/random_user_ui/RandomUserActivityTest_TestComponentDataSupplier.java:8: error: cannot find symbol import dagger.hilt.android.internal.testing.root.DaggerRandomUserActivityTest_HiltComponents_SingletonC; ^ symbol: class DaggerRandomUserActivityTest_HiltComponents_SingletonC location: package dagger.hilt.android.internal.testing.root

and /Users/shaheer/AndroidStudioProjects/ss/app/build/generated/hilt/component_sources/debugAndroidTest/dagger/hilt/android/internal/testing/root/RandomUserActivityTest_HiltComponents.java:133: error: [Dagger/MissingBinding] com.ss.assignment.data.repository.RandomUserRepository cannot be provided without an @Provides-annotated method. public abstract static class SingletonC implements SunAndSandsApp_GeneratedInjector, ^ com.ss.assignment.data.repository.RandomUserRepository is injected at com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel(randomUserRepository) com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel is injected at com.ss.assignment.ui.random_user_ui.RandomUserSharedViewModel_HiltModules.BindsModule.binds(arg0) @dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.String,javax.inject.Provider<androidx.lifecycle.ViewModel>> is requested at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.SingletonC → dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.ActivityRetainedC → dagger.hilt.android.internal.testing.root.RandomUserActivityTest_HiltComponents.ViewModelC] @Inject lateinit var mockRandomUserRepository: RandomUserRepositoryImplTest. in the above solution, I saw you suggest to do a field injection but the actual injection is happening in ViewModel. will it make sense!.

the code implementation is like


@AndroidEntryPoint
class RandomUserListFragment : Fragment(R.layout.fragment_random_user_list) {
    private var _binding: FragmentRandomUserListBinding? = null
    private val sharedViewModel: RandomUserSharedViewModel by activityViewModels()

    @Inject
   lateinit var glideRequestManager: RequestManager
...
}
@HiltViewModel
class RandomUserSharedViewModel @Inject constructor( val randomUserRepository: RandomUserRepository) :
    ViewModel() {
....
}

so by mocking the repository and injecting it into the viewmodel. I meant to test the behavior of the UI - success response, exception...

Thank you

bcorso commented 2 years ago

@sheiapp the entry point from the error message is coming from a component in RandomUserActivityTest, but the test in your snippet is RandomUserListFragmentTest. You will have to add the bindings to that test as well (though, if you end up doing this for every test you should also look into @TestInstallIn).

in the above solution, I saw you suggest to do a field injection but the actual injection is happening in ViewModel. will it make sense!.

It should be fine to inject RandomUserRepositoryImplTest into your test since the test is providing the binding into the SingletonComponent.

sheiapp commented 2 years ago

Thank you so much @bcorso it is working. If you don't mind, I'd like to clear up one last misunderstanding with you. ie, if I do want to use @BindValue then how would be the implementation in this scenario.

bcorso commented 2 years ago

ie, if I do want to use @BindValue then how would be the implementation in this scenario.

You would use the nested TestModule class in the snippet above. (sorry, still not sure that's answering your question)

bcorso commented 2 years ago

Ah sorry, I read your previous comment wrong (I thought you said "do not want to use").

If you want to use @BindValue, you could probably use the module just for the @Binds. You could try something like this:

@HiltAndroidTest
@UninstallModules(AppModule::class)
@RunWith(AndroidJUnit4::class)
class RandomUserListFragmentTest {
  @Module
  @InstallIn(SingletonComponent::class)
  interface TestModule {
    @Binds fun bind(impl: RandomUserRepositoryImplTest): RandomUserRepository
  }

  @BindValue
  @JvmField
  val repository: RandomUserRepositoryImplTest = mock()

  // ...
}
sheiapp commented 2 years ago

Thank you so much @bcorso