Issue
I have an Android app where the codebase is split into 2 different modules: App and Domain
Goal: I am attempting to use the Hilt provided testing functionality to replace a Domain internal dependency when creating App tests.
In Domain, I have an internal interface with an internal implementation, like below:
internal interface Database {
fun add(value: String)
}
internal class DatabaseImpl @Inject constructor() : Database {
override fun add(value: String) { ... }
}
The above guarantees that the Database can only be used inside Domain and cannot be accessed from elsewhere.
In Domain I have another interface (which is not internal), for use in App, with an internal implementation, like below:
interface LoginService {
fun userLogin(username: String, password: String)
}
internal class LoginServiceImpl @Inject constructor(database: Database) {
override fun userLogin(username: String, password: String) {
// Does something with the Database in here
}
}
In Domain I use Hilt to provide dependencies to App, like below:
@Module(includes = [InternalDomainModule::class]) // <- Important. Allows Hilt access to the dependencies provided by InternalDomainModule.
@InstallIn(SingletonComponent::class)
class DomainModule {
...
}
@Module
@InstallIn(SingletonComponent::class)
internal class InternalDomainModule {
@Provides
@Singleton
fun provideDatabase() : Database = DatabaseImpl()
@Provides
@Singleton
fun provideLoginService(database: Database) : LoginService = LoginServiceImpl(database)
}
This all works perfectly in isolating my implementations and only exposes a single interface outside of Domain.
However, when I need to provide fake implementations inside App using the Hilt guidelines, I am unable to replace the LoginService
as I do not have access to InternalDomainModule
(because it is internal to Domain only) and replacing DomainModule
does not replace LoginService
(as it is provided in another Hilt module, namely InternalDomainModule
), like below:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [DomainModule::class] // [InternalDomainModule::class] is impossible as inaccessible in **App**
)
class FakeModule {
@Provides
@Singleton
fun provideFakeLoginService() : LoginService = FakeLoginServiceImpl() <- Something fake
}
The above leads to only DomainModule being replaced, not InternalDomainModule, which leads to LoginService being provided twice, which makes Hilt unhappy.
Making things not internal to Domain fixes the issue, but defeats the purpose of having a multi-module Android app with clear separations.
Solution
Solution 1:
I would make the internal LoginService
in some way different for Dagger than the public one. For example, add a qualifier to it:
@Module(includes = [InternalDomainModule::class])
@InstallIn(SingletonComponent::class)
class DomainModule {
@Provides
@Singleton
fun provideLoginService(
@Named("Internal") internalLoginService: LoginService
) : LoginService = internalLoginService
}
@Module
@InstallIn(SingletonComponent::class)
internal class InternalDomainModule {
@Provides
@Singleton
fun provideDatabase() : Database = DatabaseImpl()
@Provides
@Singleton
@Named("Internal")
fun provideLoginService(database: Database) : LoginService = LoginServiceImpl(database)
}
This way, it won't be duplicated in tests.
Solution 2:
Even better, you could implement a LoginServiceFactory
:
interface LoginServiceFactory {
fun create(): LoginService
}
internal class LoginServiceFactoryImpl @Inject constructor(
private val database: Database
) : LoginServiceFactory {
override fun create(): LoginService =
LoginServiceImpl(database)
}
Then your modules would look like this:
@Module(includes = [InternalDomainModule::class])
@InstallIn(SingletonComponent::class)
class DomainModule {
@Provides
@Singleton
fun provideLoginService(loginServiceFactory: LoginServiceFactory) : LoginService =
loginServiceFactory.create()
}
@Module
@InstallIn(SingletonComponent::class)
internal class InternalDomainModule {
@Provides
@Singleton
fun provideDatabase() : Database = DatabaseImpl()
@Provides
@Singleton
fun provideLoginServiceFactory(database: Database) : LoginServiceFactory =
LoginServiceFactoryImpl(database)
}
Answered By - Slav
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.