Dagger 刀柄测试 EntryPointAccessors

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

嗨,我有一个类,它实现了带有 Foo 类型参数的抽象,并通过 EntryPointAccessors 直接提供到抽象类中。像这样的东西

SomethingImportantBuilderImpl(
    ...
): AbstractSomethingImportantBuilder(
    foo: Foo = EntryPointAccessors.fromApplication(applicationContext, FooEntryPoint::class.java).provideFoo()
)

一切都按预期工作,但现在我如何在单元测试中通过 EntryPointAccessor 向 SomethingImportantBuilder 提供 Foo

每次运行测试时都会显示下一个:

Could not find an Application in the given context: null
java.lang.IllegalStateException: Could not find an Application in the given context: null
    at dagger.hilt.android.internal.Contexts.getApplication(Contexts.java:42)
    at dagger.hilt.android.EntryPointAccessors.fromApplication(EntryPointAccessors.java:37)

有什么帮助/建议吗?如何在单元测试中提供这种依赖关系?也使用Mockk。

编辑1:如果我尝试像这样嘲笑它:

every { EntryPointAccessors.fromApplication(any(), FooEntryPoint::class.java).provideFoo() } returns mockk(relaxed = true)

抛出下一个错误:

android.content.Context.getApplicationContext()Landroid/content/Context;

编辑2:尝试使用Mockk,所以我模拟了FooEntryPoint接口,其中hilt在Singleton/Context接口中实现了这个入口点。不起作用,因为它没有初始化上下文:

private lateinit var context: Application

@Before
fun setup(){
    context = mockk(moreInterfaces = arrayOf(FooEntryPoint::class, GeneratedComponent::class), relaxed = true)
}

@Test
fun useContext(){
    println(context) //using context throws the error saying that lateinit var is not initialized.
}

错误

lateinit property context has not been initialized kotlin.UninitializedPropertyAccessException: lateinit property context has not been initialized

还是同样的问题。如何为 EntryPoint 提供 applicationContext?

android unit-testing android-testing dagger-hilt mockk
2个回答
2
投票

您面临两个不同的问题:

  1. 在单元测试中使用 Hilt
  2. 需要在 UnitTest 中注入 applicationContext

ad 1)我 90% 确定你不能在单元测试中使用 Hilt,但如果有人提供不同的信息,我会很乐意更新这个答案。 为什么不能在 UnitTests 中使用 Hilt?出于与第 2 点相同的原因(见下文),因为 Hilt 与 AndroidFramework 紧密耦合。

ad 2) 这绝对是不可能的,实际上是 Android 上的 UnitTests 和仪器测试之间的主要区别:后者在 Android 环境(真实设备、模拟器等)中运行。因此,可以测试使用 Android 类的代码组件。最著名的是

ApplicationContext
Context
Activity
Fragment
。另一方面,单元测试是原始逻辑测试,与任何 Android 框架组件无关。 UnitTest 不需要 Android 类。没有 Android 类可以用于 UnitTests。

因此,在您的情况下,您希望使用 Hilt 进行需要注入 ApplicationContext 的测试。两者都是强有力的指标,表明您必须在仪器测试中做到这一点。以下是如何为仪器测试设置刀柄的分步说明,如本视频中所述:

https://www.youtube.com/watch?v=nOp_CEP_EjM

  1. 添加以下 gradle 依赖项:
androidTestImplementation "com.google.dagger:hilt-android-testing:2.38.1" 
kaptAndroidTest "com.google.dagger:hilt-android-comiler:2.38.1"
  1. 要使用 Hilt,您需要将
    AndroidJUnitRunner
    替换为小型定制 Runner。为此,请创建此
    HiltTestRunner
    :
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication

class HiltTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}
  1. 要告诉 gradle 有关新测试运行程序的信息,请打开
    /app/build.gradle
    并交换此行:

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

使用您刚刚创建的

HiltTestRunner
的类引用:

testInstrumentationRunner "your.package.HiltTestRunner"

基本上就是这样。现在,您可以在测试文件夹中创建一个新模块,就像在普通应用程序文件夹中一样。例如:

@Module
@InstallIn(SingletonComponent::class)
class TestDatabaseModule {
    
    @Provides
    @Named("test_database")
    fun provideDatabase(@ApplicationContext applicationContext: Context): MyDatabase {
        return Room.inMemoryDatabaseBuilder(applicationContext, MyDatabase::class.java)
            .allowMainThreadQueries()
            .build()
    }
}

要将此数据库注入到您的 TestCode 中,您需要创建一个

HiltAndroidRule
并在
hiltRule.inject()
方法中调用
@Before

@HiltAndroidTest
class UserDaoTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @get:Rule
    val testCoroutineRule = TestCoroutineRule()

    @Inject
    lateinit var database: MyDatabase

    private lateinit var sut: UserDao

    @Before
    fun createDb() {
        hiltRule.inject()
        sut = database.userDao()
    }

    @After
    @Throws(IOException::class)
    fun closeDb() {
        database.clearAllTables()
        database.close()
    }

    @Test
    @Throws(Exception::class)
    fun insertAndGetUserFromDb_success() = testCoroutineRule.runBlockingTest {

        // Given a database with 1 user
        val newUser = UserFakes.get()

        sut.insert(newUser)

        // When fetching the user
        sut.get().test {

            // Then the user fields should match 
            val user = awaitItem()
            assert(user.id == newUser.id)
            assert(user.name == newUser.name)
        }

    }
}

Additionsl treat:我正在使用 Turbine 来简化测试流程。这是上面代码中的部分,我在 Flow 上调用

.test {}
并调用
awaitItem()
来等待 Flow 的结果。请参阅 Turbine readme.md 了解更多信息。


0
投票

我能做到。在这种情况下,您根本不需要 applicationContext,因为您只需要模拟

EntryPointAccessors.fromApplication

这里的关键是使用

mockkStatic

mockkStatic(EntryPointAccessors::class)
every { EntryPointAccessors.fromApplication(any(), FooEntryPoint::class.java) } returns mockk(relaxed = true) {
    every { provideFoo() } returns mockk() // Further provide customized 'Foo' instance if needed
}

你应该可以走了。

© www.soinside.com 2019 - 2024. All rights reserved.