Hilt 测试 - 替换单独的 Android 多模块应用程序中的内部 Hilt 模块

Bam*_*Bam 8 testing android kotlin dagger-hilt

我有一个 Android 应用程序,其中代码库分为 2 个不同的模块:AppDomain

目标:我尝试在创建应用程序测试时使用 Hilt 提供的测试功能来替换域内部依赖项。

Domain中,我有一个带有内部实现的内部接口,如下所示:

internal interface Database {
    fun add(value: String)
}

internal class DatabaseImpl @Inject constructor() : Database {
    override fun add(value: String) { ... }
}
Run Code Online (Sandbox Code Playgroud)

以上保证了数据库只能在Domain内部使用,不能从其他地方访问。

Domain中,我有另一个接口(不是内部的),用于App,具有内部实现,如下所示:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

Domain中,我使用 Hilt 为App提供依赖项,如下所示:

@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)
}
Run Code Online (Sandbox Code Playgroud)

这一切都完美地隔离了我的实现,并且只公开了Domain之外的单个接口。

但是,当我需要使用Hilt 指南在应用程序内部提供虚假实现时,我无法替换,因为我无权访问(因为它仅是域内部的),并且替换不会替换(因为它是在另一个中提供的) Hilt模块,即),如下:LoginServiceInternalDomainModuleDomainModuleLoginServiceInternalDomainModule

@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
}
Run Code Online (Sandbox Code Playgroud)

上面导致替换了DomainModule,而不替换InternalDomainModule,这就导致LoginService被提供了两次,这让Hilt不高兴。

让东西不在Domain内部可以解决这个问题,但违背了具有清晰分离的多模块 Android 应用程序的目的。

Sla*_*lav 4

解决方案一:

我会让LoginServiceDagger 的内部部分与公共部分有所不同。例如,为其添加限定符:

@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)
}
Run Code Online (Sandbox Code Playgroud)

这样,它就不会在测试中重复。

解决方案2:

更好的是,您可以实现LoginServiceFactory

interface LoginServiceFactory {
    fun create(): LoginService
}

internal class LoginServiceFactoryImpl @Inject constructor(
    private val database: Database
) : LoginServiceFactory {
    override fun create(): LoginService =
        LoginServiceImpl(database)
}
Run Code Online (Sandbox Code Playgroud)

那么你的模块将如下所示:

@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)
}
Run Code Online (Sandbox Code Playgroud)