3. 测试环境搭建:依赖注入(Hilt/手动)、测试模块配置、JUnit 与 Compose 测试库

State Hoisting 的 UI 测试需要稳定的测试环境,关键在于:依赖注入(DI) 让 ViewModel 和 Repository 在测试中可替换;测试模块 隔离非必要的真实组件;测试库 提供断言、Compose 节点查找和异步等待能力。本章覆盖从项目配置到测试框架集成的完整搭建流程。

3.1 依赖注入方案:Hilt vs. 手动注入

维度Hilt(推荐)手动注入(轻量)
配置复杂度中等(需添加 Gradle 插件 + Application 注解)低(直接构造对象)
编译期校验有(Dagger 编译时图校验)无(运行时错误)
测试替换难度通过 @Module / @TestInstallIn 轻松替换需手动传递 Fake 对象
推荐场景中型及以上项目小型项目或快速原型
选型建议:本课程使用 Hilt + @HiltViewModel 配合 @TestInstallIn 模块替换,无需在测试中手工创建 ViewModel 工厂。若项目已使用手动 DI,参考 3.1.2 节快速改造。

3.1.1 Hilt 集成步骤(API 级配置)

Step 1——项目级 build.gradle(或 settings.gradle.kts)添加 Hilt Gradle 插件:

// 项目级 build.gradle(Groovy)
buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
    }
}

Step 2——应用级 build.gradle 添加依赖:

// app/build.gradle
plugins {
    id 'dagger.hilt.android.plugin'
}

android {
    // 常规配置...
}

dependencies {
    implementation 'com.google.dagger:hilt-android:2.51.1'
    kapt 'com.google.dagger:hilt-compiler:2.51.1'

    // 测试依赖(详见 3.3 节)
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
    kaptAndroidTest 'com.google.dagger:hilt-compiler:2.51.1'
}

Step 3——创建 Application 类并添加 @HiltAndroidApp:

@HiltAndroidApp
class TimerApplication : Application()

Step 4——测试模块替换: 定义生产模块与测试模块,使用 @TestInstallIn 覆盖。

// 生产模块:注入真实 TimerRepository
@Module
@InstallIn(SingletonComponent::class)
object TimerModule {
    @Provides
    fun provideTimerRepository(): TimerRepository = RealTimerRepository()
}

// 测试模块:注入 FakeTimerRepository(位于 androidTest 源码集)
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [TimerModule::class]
)
@Module
object FakeTimerModule {
    @Provides
    fun provideFakeTimerRepository(): TimerRepository = FakeTimerRepository()
}
常见错误:测试模块必须放在 src/androidTest/java/ 目录下,否则插桩测试时不会生效。@TestInstallIn 的 replaces 数组需明确写出被替换的生产模块类名。

3.1.2 手动注入方案(备选)

若项目未使用 Hilt,可在测试中直接构造带假依赖的 ViewModel:

// 手动创建测试 ViewModel
class TimerViewModelTest {
    private lateinit var viewModel: TimerViewModel

    @Before
    fun setUp() {
        val fakeRepo = FakeTimerRepository()
        viewModel = TimerViewModel(fakeRepo) // ViewModel 构造函数注入
    }

    @Test
    fun timerStartsAtZero() {
        assertEquals(0L, viewModel.uiState.value.elapsedTime)
    }
}

3.2 测试模块配置(androidTest 源码集)

在 src/androidTest 目录下创建与主源码相同的包结构,放置测试基类、Fake 对象和 Hilt 测试 Runner。

// androidTest/java/com/example/timer/
├── test/
│   ├── TimerViewModelTest.kt
│   └── ui/
│       ├── TimerScreenTest.kt
│       └── helpers/
│           ├── TestHiltRunner.kt
│           └── FakeTimerRepository.kt

测试 Runner 配置:

// androidTest 中的自定义 Hilt 测试 Runner
class TimerTestRunner : AndroidJUnitRunner() {
    override fun newApplication(
        cl: ClassLoader?,
        className: String?,
        context: Context?
    ): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

在 app/build.gradle 中指定自定义 Runner:

android {
    defaultConfig {
        testInstrumentationRunner "com.example.timer.test.TimerTestRunner"
        // 也可以直接使用 Hilt 提供的 Runner:
        // testInstrumentationRunner "com.google.dagger.hilt.android.testing.HiltTestRunner"
    }
}

3.3 JUnit 与 Compose 测试库

需在 app/build.gradle 中添加以下依赖(确保版本与 Compose BOM 对齐):

dependencies {
    // JUnit 5(亦可使用 JUnit 4,本课程以 JUnit 5 为例)
    androidTestImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
    androidTestImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
    androidTestImplementation 'org.junit.vintage:junit-vintage-engine:5.10.2' // 兼容 JUnit 4

    // Compose UI 测试库
    androidTestImplementation platform('androidx.compose:compose-bom:2024.06.00')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    androidTestImplementation 'androidx.compose.ui:ui-test-manifest'
    debugImplementation 'androidx.compose.ui:ui-test-manifest' // 用于 setContent 辅助

    // Hilt 测试支持(已在 3.1.1 加入)
    // androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
}

关键测试注解与类:

  • @HiltAndroidTest —— 标记测试类,开启 Hilt 组件注入。
  • @get:Rule(order = 0) —— HiltAndroidRule(this) 初始化注入。
  • createComposeRule() —— 用于启动 Compose content 的测试规则。
API 示例: 集成后的测试基类骨架
@HiltAndroidTest
class TimerScreenTest {
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createComposeRule()

    @Inject
    lateinit var fakeRepository: TimerRepository

    @Before
    fun setUp() {
        hiltRule.inject()
        // 注入的 fakeRepository 已在 @TestInstallIn 中替换为 FakeTimerRepository
    }

    @Test
    fun displayInitialState() {
        composeTestRule.setContent {
            TimerScreen(viewModel = hiltViewModel())
        }
        composeTestRule.onNodeWithText("00:00:00").assertExists()
    }
}

3.4 测试环境搭建流程总览

测试环境搭建流程图 ① Gradle 配置依赖 Hilt插件·Compose BOM·JUnit5 ② 创建 @HiltAndroidApp TimerApplication + 生产 Module ③ @TestInstallIn 替换为 Fake 模块 ④ 测试基类 + Runner 自定义 Runner / HiltTestApplication ⑤ 编写测试用例 @HiltAndroidTest·ComposeTestRule ⑥ 运行插桩测试 ./gradlew connectedDebugAndroidTest 测试报告分析 失败定位·State Hoisting 验证 ⚠️ 常见陷阱 1. 测试模块必须放在 androidTest 目录 2. @HiltAndroidTest 不可省略

3.5 常见错误与排查

错误现象原因解决方案
测试启动时 IllegalStateException: Hilt components not found未使用 @HiltAndroidTest 启动 Runner在测试类上加注解,并确认 Runner 为 TimerTestRunner 或 HiltTestRunner
FakeTimerRepository 未注入@TestInstallIn 的模块未被加载检查模块是否位于 androidTest 源码集,并确认 replaces 类名与生产模块一致
Compose 测试中 onNodeWithText 找不到元素State Hoisting 未生效 / UI 渲染被异步遮挡使用 composeTestRule.waitUntil(timeoutMs) 等待 recomposition 完成
JUnit 5 测试方法不执行缺少 junit-vintage-engine 或未配置 testInstrumentationRunnerArguments添加 androidTestImplementation 'org.junit.vintage:junit-vintage-engine:5.10.2' 并设置 Runner 为 AndroidJUnitRunner 结合 HiltTestRunner
验证环境是否正确: 可先编写一个最简单的空测试(只需 assertEquals(1, 1))运行在模拟器上,确认 JUnit + Hilt 注入均无异常,再逐步添加 Compose UI 测试。