多次实例化的 Hilt Singleton 组件

问题描述 投票:0回答:1

在我的仪器测试中,我注意到我的 Retrofit 组件是在测试甚至执行

hiltRule.inject()
命令之前创建的。

这可能是因为我正在使用 WorkManager 和早期入口点组件

open class BaseApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return Configuration.Builder().setWorkerFactory(
            EarlyEntryPoints.get(
                applicationContext,
                WorkerFactoryEntryPoint::class.java
            ).workerFactory
        ).build()
    }

    @EarlyEntryPoint
    @InstallIn(SingletonComponent::class)
    interface WorkerFactoryEntryPoint {
        val workerFactory: HiltWorkerFactory
    }
}

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

我想在我的测试和 Retrofit 拦截器中注入一个 OkHttp3 MockWebServer,这样我就可以确定正在使用哪个端口(来自

mockWebServer.start()
)并相应地设置我的模拟但是,尽管将我的 MockWebServer 包装类标记为 Singleton我可以看到正在创建它的多个实例,因此它们具有不同的端口号。

看起来它在创建应用程序时创建了一个 MockWebServer 实例,然后在注入测试时创建了另一个实例,但这可能意味着我的模拟没有正确定义。

@Singleton
class MockWebServerWrapper @Inject constructor() {

    private val mockWebServer by lazy { MockWebServer() }

    val port get() = mockWebServer.port

    fun mockRequests() {
        ...
    }
}

是否有更正确的方法在我为 WorkManager 定义的 Retrofit 拦截器和测试活动本身内的网络请求所需的拦截器之间共享相同的模拟网络服务器?

在下面 Levon 的评论之后,我对 BaseApplication 进行了更改,创建了 ApplicationInjectionExecutionRule 并更新了 BaseTest 类,以便规则如下所示:

@get:Rule(order = 0)
val disableAnimationsRule = DisableAnimationsRule()

private lateinit var hiltRule: HiltAndroidRule

@get:Rule(order = 1)
val ruleChain: RuleChain by lazy {
    RuleChain
        .outerRule(HiltAndroidRule(this).also { hiltRule = it })
        .around(ApplicationInjectionExecutionRule())
}

@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()

但我仍然看到(城市)飞艇起飞的错误,这就是为什么我将 WorkManagerConfiguration 移至 EarlyEntryPoints 的原因。

E  Scheduler failed to schedule jobInfo com.urbanairship.job.SchedulerException: Failed to schedule job at com.urbanairship.job.WorkManagerScheduler.schedule(WorkManagerScheduler.java:31)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property workerFactory has not been initialized at com.gocitypass.BaseApplication.getWorkManagerConfiguration(BaseApplication.kt:33)                                                                                       
android testing android-jetpack-compose android-workmanager dagger-hilt
1个回答
1
投票

在运行仪器测试时,Hilt 预定义的 Singleton 组件的生命周期限定为测试用例的生命周期,而不是 Application 的生命周期。这对于防止测试用例中的状态泄漏很有用。

Android Gradle 仪器测试期间的典型应用程序生命周期

  • 已创建应用程序
    • Application.onCreate() 调用
    • Test1 创建
      • SingletonComponent 创建
      • testCase1() 调用
    • Test1 创建
      • SingletonComponent 创建
      • testCase2() 调用 ...
    • Test2 创建
      • SingletonComponent 创建
      • testCase1() 调用
    • Test2 创建
      • SingletonComponent 创建
      • testCase2() 调用 ...
  • 应用被销毁

如上面的生命周期所示,

Application#onCreate()
在可以创建任何
SingletonComponent
之前被调用,因此在运行仪器测试时不可能从应用程序中的 Hilt 预定义单例组件注入绑定。为了绕过此限制,Hilt 提供了一个逃生通道 (EarlyEntryPoint) 以在创建 Hilt 的预定义单例组件之前在应用程序中请求绑定。

使用

EarlyEntryPoint
有一些注意事项。正如您提到的,通过
EarlyEntryPoint
检索的单例范围绑定和从 Hilt 的预定义单例组件检索的相同绑定在运行检测测试时检索单例范围绑定的不同实例。

幸运的是 Hilt 提供了 OnComponentReadyListener API,可以在自定义测试规则中注册,一旦 Hilt Singleton 组件准备就绪,它就会通知。这允许我们延迟

BaseApplication
中的注入执行代码,并在测试规则中运行它。
EarlyEntryPoints
中的
BaseApplication
现在可以更改为
EntryPoints
,因为我们不会在仪器测试中创建
Singleton
组件之前尝试访问绑定。

BaseApplication.kt

    open class BaseApplication : Application(), Configuration.Provider {

        private lateinit var workerFactory: HiltWorkerFactory

        override fun onCreate() {
            super.onCreate()
            if (!isUnderAndroidTest()) {
                excecuteInjection()
            }
        }

        fun excecuteInjection() {
            workerFactory = EntryPoints.get(
                applicationContext,
                WorkerFactoryEntryPoint::class.java
            ).workerFactory
        } 
    
        override fun getWorkManagerConfiguration(): Configuration {
            return Configuration.Builder().setWorkerFactory(workerFactory).build()
        }
    
        @EntryPoint
        @InstallIn(SingletonComponent::class)
        interface WorkerFactoryEntryPoint {
            val workerFactory: HiltWorkerFactory
        }

        @Suppress("SwallowedException")
        private fun isUnderAndroidTest(): Boolean {
            return try {
                Class.forName("androidx.test.espresso.Espresso")
                true
            } catch (e: ClassNotFoundException) {
                false
            }
        }
    }

ApplicationInjectionExecutionRule.kt

import androidx.test.core.app.ApplicationProvider
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import dagger.hilt.android.testing.OnComponentReadyRunner
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

class ApplicationInjectionExecutionRule : TestRule {

    private val targetApplication: BaseApplication
        get() = ApplicationProvider.getApplicationContext()

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            override fun evaluate() {
                OnComponentReadyRunner.addListener(
                    targetApplication, WorkerFactoryEntryPoint::class.java
                ) { entryPoint: WorkerFactoryEntryPoint ->
                    runOnUiThread { targetApplication.excecuteInjection() }
                }
                base.evaluate()
            }
        }
    }
}

请注意,仅当 HiltAndroidRule

first
运行时,使用 OnComponentReadyListener 的测试规则才会按预期工作,例如

@Rule
@JvmField
val ruleChain = RuleChain
    .outerRule(hiltRule)
    .around(ApplicationInjectionExecutionRule())
© www.soinside.com 2019 - 2024. All rights reserved.