google / dagger

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

Hilt: Replace binding from different gradle module in tests #3605

Closed ampeixoto closed 9 months ago

ampeixoto commented 1 year ago

Hello,

I would like to know if it is possible to replace a binding from a hilt module, while this hilt module is in another gradle module and has the internal modifier?

Basically, imagine the following situation:

In module :my-lib I have 3 classes:

interface MyService {
    fun doSomething(value: String)
}
internal class MyServiceImpl @Inject constructor() : MyService  {
    override fun doSomething(value: String) { ... }
}
@Module
@InstallIn(SingletonComponent::class)
internal object MyServiceModule {

    @Provides
    fun providesMyService(impl: MyServiceImpl): MyService = impl
}

In the module :app I'm able to inject MyService wherever I want in the production code.

However, when I try to replace in some test class in :app:

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@MediumTest
internal class SomeClassTest {

    @BindValue
    @JvmField
    val myServiceMock: MyService = mockk()

   //....

}

Of course I get a compilation error saying is bound multiple times. One due to the MyServiceModule and another one due to the @BindValue in the test.

And I am not able to use@UninstallModules and @TestInstallIn annotations since the MyService hilt module is internal.

So Is there any workaround (or proper way to do it) that I am not aware of?

(BTW, this is very similar to this stackoverflow issue)

bcorso commented 1 year ago

Hi @ampeixoto,

The simplest solution is to just make the module public. If users need to replace the module for testing then it seems reasonable to consider the module part of the public API.

Another (usually more difficult) option is to split foo library into 2 separate Gradle libraries, e.g.

  1. foo - API only
  2. foo-impl - implementation and Hilt modules

Then have your libraries depend on just the API, :foo, and the tests can then @BindValue whatever they want since the Hilt modules are not included. Your Gradle application would need to depend on foo-impl so that the implementation is provided when generating the components for your app.

However, for most cases the simple option of just making your module public should be good enough.

ampeixoto commented 10 months ago

Hi @bcorso!

Sorry for the (looong) delay, but I completely forgot about this. However, I recently faced the problem again and remembered I opened this ticket.

The problem with the simpler solution you propose, that that I really don't want to the module to expose the implementation... And the second solution is a bit too cumbersome IMO.

Thanks for the suggestions anyway.

Meanwhile I search a bit and found a pretty good suggestion on SO that works! Here it is: https://stackoverflow.com/a/76182220/1204249

Hopefully it will help other people as well.

bcorso commented 10 months ago

If you're going that route it may be simpler to just use @DisableInstallInCheck on your internal module, e.g.

@Module(includes = [InternalDomainModule::class])
@InstallIn(SingletonComponent::class)
class DomainModule

@Module
@DisableInstallInCheck // This annotation allows us to avoid installing this module directly.
internal class InternalDomainModule {
    @Provides
    @Singleton
    fun provideDatabase() : Database = DatabaseImpl()

    @Provides
    @Singleton
    fun provideLoginService(database: Database) : LoginService = LoginServiceImpl(database)
}

However, if you're just worried about the visibility of the provides methods, couldn't you just make the methods themselves internal, e.g. ?

@Module
@InstallIn(SingletonComponent::class)
class DomainModule {
    @Provides
    @Singleton
    internal fun provideDatabase() : Database = DatabaseImpl()

    @Provides
    @Singleton
    internal fun provideLoginService(database: Database) : LoginService = LoginServiceImpl(database)
}
ampeixoto commented 9 months ago

@bcorso Thanks for the suggestions!

I didn't know about the first one, but the second one is exactly what I want, and works perfectly!

How can I not have thought about that solution??

We can close this :)