嗨,我有一个类,它实现了带有 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?
您面临两个不同的问题:
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
androidTestImplementation "com.google.dagger:hilt-android-testing:2.38.1"
kaptAndroidTest "com.google.dagger:hilt-android-comiler:2.38.1"
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)
}
}
/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 了解更多信息。
我能做到。在这种情况下,您根本不需要 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
}
你应该可以走了。