第14章:Hilt测试:@HiltAndroidTest、测试模块的配置、单元测试与集成测试
测试这件事,说实话在依赖注入框架里是最容易被忽视的。很多人觉得「反正Hilt帮我管理对象了,测试应该很简单吧?」——嗯,简单是简单,但坑也不少。我在几个大型项目里踩过测试的坑,今天把这些经验都倒出来给你。
14.1 为什么Hilt测试需要特殊处理?
先想一个问题:你的Activity或Fragment里用了@Inject、@HiltViewModel这些注解,测试的时候谁来提供这些依赖?
普通单元测试跑不起来,因为Hilt的注解处理器只在编译期生成代码。测试环境里没有Application组件,没有ActivityRetainedComponent,你手动new一个对象?那依赖注入的意义就没了。
所以Google专门搞了一套测试支持库——@HiltAndroidTest。说白了,它让测试用例也能享受Hilt的依赖注入能力。
核心思想:测试也是一个「Android组件」,Hilt为测试创建独立的测试组件树。
14.2 @HiltAndroidTest 注解
这个注解是Hilt测试的入口。用法很简单:
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var viewModel: MainViewModel
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testViewModel() {
assertNotNull(viewModel)
}
}
注意几个关键点:
- 必须加
@HiltAndroidTest,否则Hilt不会为测试生成组件 - 必须加
HiltAndroidRule,它负责在测试前初始化Hilt - 调用
hiltRule.inject(),手动触发字段注入
我曾经见过有人忘了调inject(),结果viewModel一直是null,排查了半天……嗯,这个坑你记住就好。
14.3 测试模块的配置
测试环境往往需要替换掉一些真实依赖。比如网络请求、数据库、或者一些第三方SDK。Hilt允许你为测试创建单独的Module。
14.3.1 测试Module的创建
在src/test或src/androidTest目录下创建Module:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
object FakeNetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return FakeApiService() // 返回假实现
}
}
这里有个关键注解——@TestInstallIn。它告诉Hilt:这个Module只在测试时生效,并且要替换掉原来的NetworkModule。
我的习惯:每个真实Module都配一个Fake版本。这样测试时想替换哪个就替换哪个,灵活度很高。
14.3.2 测试Module的作用域
测试Module可以安装到不同的组件中:
| 组件 | 作用域 | 适用场景 |
|---|---|---|
| SingletonComponent | 整个测试过程 | 全局依赖替换(如网络、数据库) |
| ActivityComponent | 单个Activity测试 | UI相关的依赖替换 |
| FragmentComponent | 单个Fragment测试 | Fragment级别的依赖替换 |
你想想看,如果只是某个Activity测试需要替换依赖,你把它装到SingletonComponent里,那其他测试也会受影响。所以作用域要选对。
14.4 单元测试 vs 集成测试
这两个概念很多人分不清。我简单说一下:
- 单元测试:测试单个类,不启动Android组件。用
src/test目录。 - 集成测试:测试多个组件协作,需要启动Activity/Fragment。用
src/androidTest目录。
14.4.1 单元测试实战
单元测试里,Hilt的作用主要是提供依赖的Mock或Fake对象。
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class UserRepositoryTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var repository: UserRepository
@Inject
lateinit var fakeApi: FakeApiService
@Before
fun setup() {
hiltRule.inject()
// 配置Fake的行为
fakeApi.setResponse(Result.success(User("test")))
}
@Test
fun testGetUser() {
val user = repository.getUser("1")
assertEquals("test", user.name)
}
}
这里有个细节:FakeApiService是通过测试Module注入的,所以repository内部使用的就是Fake实现。你不需要手动去设置repository.apiService = fakeApi——Hilt帮你搞定了。
注意:单元测试里不要启动Activity!否则测试速度会慢很多。我见过有人把单元测试写成集成测试,跑一次要几分钟……
14.4.2 集成测试实战
集成测试需要启动真实的Android组件。Hilt提供了@HiltAndroidTest + ActivityScenario的组合。
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Test
fun testActivityLaunch() {
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity { activity ->
assertNotNull(activity.viewModel)
}
}
}
集成测试里,Hilt会为Activity创建完整的组件树。你不需要手动注入——Activity自己会通过@AndroidEntryPoint完成注入。
但要注意:集成测试的Module配置和单元测试是共享的。如果你在src/test和src/androidTest都定义了测试Module,Hilt会优先使用src/androidTest里的。
14.5 测试中的常见陷阱
我整理了几个高频问题:
- 测试Module不生效:检查
@TestInstallIn的replaces属性是否写对了Module类名 - 注入字段为null:确认调用了
hiltRule.inject() - 测试速度慢:尽量用单元测试,少用集成测试
- 依赖循环:测试Module里也可能出现循环依赖,用
@Lazy或@Provider解决
避坑指南:我曾经在测试Module里忘了写@Singleton,结果每次注入都创建新对象,导致Fake的状态丢失。排查了整整一下午……所以,测试Module的作用域一定要和原Module保持一致。
14.6 知识体系总览
下面这张图总结了Hilt测试的核心结构:
14.7 小结
Hilt测试的核心就三件事:
- 用
@HiltAndroidTest开启测试注入能力 - 用
@TestInstallIn配置测试Module,替换真实依赖 - 区分单元测试和集成测试,选择合适的测试策略
说实话,Hilt的测试支持做得相当成熟。你只要把Module配置好,剩下的就是写业务逻辑的测试代码。比起手动管理依赖的时代,这已经幸福太多了。
我的建议:从项目一开始就把测试Module建好。别等到代码写完了再补测试——那时候你连依赖关系都理不清了。