第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/testsrc/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/testsrc/androidTest都定义了测试Module,Hilt会优先使用src/androidTest里的。

14.5 测试中的常见陷阱

我整理了几个高频问题:

  1. 测试Module不生效:检查@TestInstallInreplaces属性是否写对了Module类名
  2. 注入字段为null:确认调用了hiltRule.inject()
  3. 测试速度慢:尽量用单元测试,少用集成测试
  4. 依赖循环:测试Module里也可能出现循环依赖,用@Lazy@Provider解决

避坑指南:我曾经在测试Module里忘了写@Singleton,结果每次注入都创建新对象,导致Fake的状态丢失。排查了整整一下午……所以,测试Module的作用域一定要和原Module保持一致。

14.6 知识体系总览

下面这张图总结了Hilt测试的核心结构:

Hilt测试知识体系 @HiltAndroidTest 测试Module配置 单元测试 vs 集成测试 HiltAndroidRule 手动调用 inject() 测试组件树 @TestInstallIn replaces 属性 作用域选择 单元测试:src/test 集成测试:src/androidTest ActivityScenario 核心原则:测试环境也是Hilt管理的,用测试Module替换真实依赖 单元测试测逻辑,集成测试测交互,两者互补

14.7 小结

Hilt测试的核心就三件事:

  • @HiltAndroidTest开启测试注入能力
  • @TestInstallIn配置测试Module,替换真实依赖
  • 区分单元测试和集成测试,选择合适的测试策略

说实话,Hilt的测试支持做得相当成熟。你只要把Module配置好,剩下的就是写业务逻辑的测试代码。比起手动管理依赖的时代,这已经幸福太多了。

我的建议:从项目一开始就把测试Module建好。别等到代码写完了再补测试——那时候你连依赖关系都理不清了。

公众号:蓝海资料掘金营,微信deep3321